@karni/ui 0.3.1 → 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.
- package/README.md +712 -72
- package/dist/index.cjs +5 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +8660 -4725
- package/dist/index.js.map +1 -1
- package/dist/manifest.json +18 -1
- package/dist/script-BVJ9BbPm.cjs +2 -0
- package/dist/script-BVJ9BbPm.cjs.map +1 -0
- package/dist/script-D1fn4Dm4.js +6 -0
- package/dist/script-D1fn4Dm4.js.map +1 -0
- package/dist/src/auth/AuthGuard.d.ts +69 -4
- package/dist/src/auth/LoginForm.d.ts +41 -7
- package/dist/src/auth/engine.d.ts +15 -13
- package/dist/src/auth/provider.d.ts +10 -10
- package/dist/src/auth/storage.d.ts +1 -1
- package/dist/src/auth/types.d.ts +3 -3
- package/dist/src/components/brand/karni-mark.d.ts +4 -4
- package/dist/src/components/chart/data-chart.d.ts +66 -11
- package/dist/src/components/empty-state/empty-state.d.ts +22 -2
- package/dist/src/components/file-upload/FileUpload.d.ts +67 -3
- package/dist/src/components/layout/layout.d.ts +104 -9
- package/dist/src/components/layout/route-focus.d.ts +43 -0
- package/dist/src/components/modal/modal.d.ts +49 -8
- package/dist/src/components/page/page.d.ts +36 -5
- package/dist/src/components/primitives/button.d.ts +1 -0
- package/dist/src/components/primitives/checkbox.d.ts +4 -1
- package/dist/src/components/primitives/dropdown-menu.d.ts +1 -1
- package/dist/src/components/primitives/icons.d.ts +9 -1
- package/dist/src/components/primitives/input.d.ts +4 -1
- package/dist/src/components/primitives/label.d.ts +4 -1
- package/dist/src/components/primitives/select.d.ts +37 -6
- package/dist/src/components/primitives/skeleton.d.ts +4 -1
- package/dist/src/components/primitives/switch.d.ts +4 -1
- package/dist/src/components/primitives/tabs.d.ts +2 -2
- package/dist/src/components/primitives/textarea.d.ts +4 -1
- package/dist/src/components/resource/ResourceDetail.d.ts +3 -3
- package/dist/src/components/resource/ResourceForm.d.ts +4 -3
- package/dist/src/components/resource/ResourceSelect.d.ts +3 -1
- package/dist/src/components/resource/ResourceTable.d.ts +9 -3
- package/dist/src/components/resource/audit-fields.d.ts +1 -1
- package/dist/src/components/resource/detail/ResourceDetail.d.ts +109 -11
- package/dist/src/components/resource/empty-value.d.ts +17 -0
- package/dist/src/components/resource/form/ResourceForm.d.ts +65 -6
- package/dist/src/components/resource/form/field-inputs.d.ts +15 -1
- package/dist/src/components/resource/form/form-states.d.ts +87 -0
- package/dist/src/components/resource/form/prepare.d.ts +3 -3
- package/dist/src/components/resource/form/server-errors.d.ts +14 -0
- package/dist/src/components/resource/history/ChangeHistory.d.ts +46 -4
- package/dist/src/components/resource/import/ResourceImport.d.ts +73 -7
- package/dist/src/components/resource/nouns.d.ts +12 -0
- package/dist/src/components/resource/query-state.d.ts +20 -0
- package/dist/src/components/resource/resource-names.d.ts +23 -0
- package/dist/src/components/resource/select/ResourceSelect.d.ts +39 -8
- package/dist/src/components/resource/select/select-states.d.ts +68 -0
- package/dist/src/components/resource/select/use-option-pages.d.ts +37 -0
- package/dist/src/components/resource/select/venue-backend.d.ts +144 -0
- package/dist/src/components/resource/table/BulkActionsBar.d.ts +62 -5
- package/dist/src/components/resource/table/CardList.d.ts +31 -0
- package/dist/src/components/resource/table/ConfirmDialog.d.ts +28 -0
- package/dist/src/components/resource/table/ExportButtons.d.ts +2 -2
- package/dist/src/components/resource/table/FilterBar.d.ts +3 -3
- package/dist/src/components/resource/table/PaginationControls.d.ts +15 -3
- package/dist/src/components/resource/table/ResourceTable.d.ts +79 -14
- package/dist/src/components/resource/table/RowCard.d.ts +31 -0
- package/dist/src/components/resource/table/TableView.d.ts +29 -0
- package/dist/src/components/resource/table/ViewOptions.d.ts +19 -0
- package/dist/src/components/resource/table/access.d.ts +12 -0
- package/dist/src/components/resource/table/derive-columns.d.ts +2 -2
- package/dist/src/components/resource/table/grid-navigation.d.ts +23 -0
- package/dist/src/components/resource/table/layout.d.ts +37 -0
- package/dist/src/components/resource/table/preferences.d.ts +12 -0
- package/dist/src/components/resource/table/slots.d.ts +41 -0
- package/dist/src/components/resource/table/states.d.ts +91 -0
- package/dist/src/components/resource/table/story-fixtures.d.ts +25 -0
- package/dist/src/components/resource/table/structural-columns.d.ts +25 -0
- package/dist/src/components/resource/table/table-icons.d.ts +16 -0
- package/dist/src/components/resource/table/table-utils.d.ts +27 -3
- package/dist/src/components/resource/table/url-state.d.ts +67 -0
- package/dist/src/components/resource/table/use-container-width.d.ts +10 -0
- package/dist/src/components/resource/testapp-fixtures.d.ts +78 -0
- package/dist/src/components/stat/stat.d.ts +36 -6
- package/dist/src/components/toast/toast-store.d.ts +1 -1
- package/dist/src/components/toast/toaster.d.ts +39 -6
- package/dist/src/components/toast/use-toast.d.ts +1 -1
- package/dist/src/hooks/keys.d.ts +4 -4
- package/dist/src/hooks/multipart.d.ts +9 -9
- package/dist/src/hooks/params.d.ts +1 -1
- package/dist/src/hooks/types.d.ts +3 -3
- package/dist/src/hooks/useChangeHistory.d.ts +3 -2
- package/dist/src/hooks/useResourceImport.d.ts +2 -2
- package/dist/src/hooks/useResourceMutations.d.ts +2 -2
- package/dist/src/hooks/useUnsavedChangesGuard.d.ts +31 -0
- package/dist/src/index.d.ts +49 -31
- package/dist/src/lib/roles.d.ts +4 -4
- package/dist/src/lib/variants.d.ts +1 -1
- package/dist/src/primitives/registry.d.ts +81 -0
- package/dist/src/schema/client.d.ts +10 -10
- package/dist/src/schema/parse.d.ts +2 -2
- package/dist/src/schema/provider.d.ts +10 -3
- package/dist/src/schema/types.d.ts +18 -9
- package/dist/src/schema/useSchema.d.ts +5 -1
- package/dist/src/states/ErrorState.d.ts +43 -0
- package/dist/src/states/PendingButton.d.ts +16 -0
- package/dist/src/states/describe-error.d.ts +25 -0
- package/dist/src/states/hooks.d.ts +10 -0
- package/dist/src/theme/script.d.ts +12 -0
- package/dist/src/theme/theme.d.ts +45 -0
- package/dist/styles.css +174 -13
- package/dist/tailwind-preset.cjs +2 -2
- package/dist/tailwind-preset.cjs.map +1 -1
- package/dist/tailwind-preset.d.ts +83 -1
- package/dist/tailwind-preset.js +181 -62
- package/dist/tailwind-preset.js.map +1 -1
- package/dist/tailwind-v4.css +31 -0
- package/dist/vite-plugin.cjs +2 -0
- package/dist/vite-plugin.cjs.map +1 -0
- package/dist/vite-plugin.d.ts +19 -0
- package/dist/vite-plugin.js +16 -0
- package/dist/vite-plugin.js.map +1 -0
- package/eslint-plugin/index.js +4 -0
- package/eslint-plugin/rules/max-page-lines.js +1 -1
- package/eslint-plugin/rules/no-deep-imports.js +1 -1
- package/eslint-plugin/rules/no-direct-http.js +1 -1
- package/eslint-plugin/rules/no-raw-elements.js +1 -1
- package/eslint-plugin/rules/no-style-literals.js +222 -0
- package/eslint-plugin/rules.json +10 -4
- package/package.json +22 -19
package/dist/src/lib/roles.d.ts
CHANGED
|
@@ -8,12 +8,12 @@
|
|
|
8
8
|
* duplicate via a static scan of these call sites.
|
|
9
9
|
*
|
|
10
10
|
* Conventions:
|
|
11
|
-
* - The `role` argument MUST be a string literal
|
|
11
|
+
* - The `role` argument MUST be a string literal. The build-time scan rejects
|
|
12
12
|
* any non-literal first argument.
|
|
13
13
|
* - Same role + same component name is idempotent (modules re-evaluate under
|
|
14
14
|
* HMR and across test files).
|
|
15
15
|
* - Replacing on purpose is explicit and must carry a reason:
|
|
16
|
-
* `{ replace: true, reason: "<why>" }
|
|
16
|
+
* `{ replace: true, reason: "<why>" }`. An empty reason throws.
|
|
17
17
|
* - Under Vite HMR (`import.meta.hot`) an accidental conflict logs a
|
|
18
18
|
* console.error and replaces instead of throwing, so a rename mid-edit does
|
|
19
19
|
* not kill the dev server.
|
|
@@ -22,11 +22,11 @@
|
|
|
22
22
|
export interface RegisterComponentRoleOptions {
|
|
23
23
|
/** Replace an existing registration on purpose. Requires `reason`. */
|
|
24
24
|
replace?: boolean;
|
|
25
|
-
/** Why the replacement is intentional
|
|
25
|
+
/** Why the replacement is intentional. Required with `replace: true`. */
|
|
26
26
|
reason?: string;
|
|
27
27
|
}
|
|
28
28
|
/**
|
|
29
|
-
* Test-only override for HMR detection
|
|
29
|
+
* Test-only override for HMR detection. Pass `null` to restore the default.
|
|
30
30
|
* Not exported from the package barrel.
|
|
31
31
|
* @internal
|
|
32
32
|
*/
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Hand-rolled cva-style variant utility (no extra dependency).
|
|
3
3
|
*
|
|
4
4
|
* Every class string appears whole in source (contract D7): a variant map
|
|
5
|
-
* value is a complete Tailwind class list, selected
|
|
5
|
+
* value is a complete Tailwind class list, selected (never concatenated)
|
|
6
6
|
* at call time and merged through cn().
|
|
7
7
|
*/
|
|
8
8
|
/** A map of variant group name → allowed value → whole Tailwind class string. */
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { ButtonProps } from '../components/primitives/button';
|
|
2
|
+
import { CheckboxProps } from '../components/primitives/checkbox';
|
|
3
|
+
import { InputProps } from '../components/primitives/input';
|
|
4
|
+
import { LabelProps } from '../components/primitives/label';
|
|
5
|
+
import { SkeletonProps } from '../components/primitives/skeleton';
|
|
6
|
+
import { SwitchProps } from '../components/primitives/switch';
|
|
7
|
+
import { TextareaProps } from '../components/primitives/textarea';
|
|
8
|
+
/**
|
|
9
|
+
* The primitive registry: a project renders Karni's components with its own
|
|
10
|
+
* design system's primitives.
|
|
11
|
+
*
|
|
12
|
+
* <KarniProvider client={client} primitives={{ button: Button, input: Input }}>
|
|
13
|
+
*
|
|
14
|
+
* Every Karni primitive (Button, Input, Select's parts, ...) looks itself up
|
|
15
|
+
* here before rendering, so ResourceTable, ResourceForm and the rest render
|
|
16
|
+
* the project's button and inputs while keeping their schema-driven
|
|
17
|
+
* behaviour. A role that is not registered renders Karni's own.
|
|
18
|
+
*
|
|
19
|
+
* Contracts: a registered component receives exactly the props Karni's
|
|
20
|
+
* primitive takes (ButtonProps, InputProps, ...) and the ref. shadcn/ui's
|
|
21
|
+
* components match them as they are. Any other design system registers a
|
|
22
|
+
* small adapter. `select` is a family: Radix parts share a context, so its
|
|
23
|
+
* parts are replaced together or not at all.
|
|
24
|
+
*
|
|
25
|
+
* A registered component may wrap Karni's own (`(props) => <Button
|
|
26
|
+
* className="..." {...props} />`): inside it, that role resolves to Karni's
|
|
27
|
+
* primitive again, so the wrapper does not call itself.
|
|
28
|
+
*/
|
|
29
|
+
import type * as SelectPrimitive from "@radix-ui/react-select";
|
|
30
|
+
import * as React from "react";
|
|
31
|
+
type WithRef<P, T> = React.ComponentType<P & React.RefAttributes<T>>;
|
|
32
|
+
/** The parts of a Radix-compatible select (what shadcn/ui's select.tsx exports). */
|
|
33
|
+
export interface KarniSelectParts {
|
|
34
|
+
Root: React.ComponentType<SelectPrimitive.SelectProps>;
|
|
35
|
+
Trigger: WithRef<SelectPrimitive.SelectTriggerProps, HTMLButtonElement>;
|
|
36
|
+
Value: WithRef<SelectPrimitive.SelectValueProps, HTMLSpanElement>;
|
|
37
|
+
Content: WithRef<SelectPrimitive.SelectContentProps, HTMLDivElement>;
|
|
38
|
+
Item: WithRef<SelectPrimitive.SelectItemProps, HTMLDivElement>;
|
|
39
|
+
Group: WithRef<SelectPrimitive.SelectGroupProps, HTMLDivElement>;
|
|
40
|
+
Label: WithRef<SelectPrimitive.SelectLabelProps, HTMLDivElement>;
|
|
41
|
+
Separator: WithRef<SelectPrimitive.SelectSeparatorProps, HTMLDivElement>;
|
|
42
|
+
}
|
|
43
|
+
/** Every role a project can fill with its own component. */
|
|
44
|
+
export interface KarniPrimitives {
|
|
45
|
+
button: WithRef<ButtonProps, HTMLButtonElement>;
|
|
46
|
+
input: WithRef<InputProps, HTMLInputElement>;
|
|
47
|
+
textarea: WithRef<TextareaProps, HTMLTextAreaElement>;
|
|
48
|
+
checkbox: WithRef<CheckboxProps, HTMLButtonElement>;
|
|
49
|
+
switch: WithRef<SwitchProps, HTMLButtonElement>;
|
|
50
|
+
label: WithRef<LabelProps, HTMLLabelElement>;
|
|
51
|
+
skeleton: WithRef<SkeletonProps, HTMLDivElement>;
|
|
52
|
+
select: KarniSelectParts;
|
|
53
|
+
}
|
|
54
|
+
/** A role that is one component (not a family of parts). */
|
|
55
|
+
export type KarniPrimitiveRole = Exclude<keyof KarniPrimitives, "select">;
|
|
56
|
+
/** The project's replacements: any subset of the roles. */
|
|
57
|
+
export type KarniPrimitiveOverrides = Partial<KarniPrimitives>;
|
|
58
|
+
export interface KarniPrimitivesProviderProps {
|
|
59
|
+
/** Role -> the project's component. Merged over any registry above it. */
|
|
60
|
+
value: KarniPrimitiveOverrides;
|
|
61
|
+
children?: React.ReactNode;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Registers the project's primitives for everything inside it. KarniProvider
|
|
65
|
+
* renders one from its `primitives` prop, and you can nest another to change
|
|
66
|
+
* a subtree.
|
|
67
|
+
*/
|
|
68
|
+
export declare function KarniPrimitivesProvider({ value, children, }: KarniPrimitivesProviderProps): React.ReactElement;
|
|
69
|
+
/** The registered primitives in scope (the roles the project replaced). */
|
|
70
|
+
export declare function useKarniPrimitives(): KarniPrimitiveOverrides;
|
|
71
|
+
/**
|
|
72
|
+
* Makes a Karni primitive defer to the registry.
|
|
73
|
+
* @internal
|
|
74
|
+
*/
|
|
75
|
+
export declare function overridable<P extends object, T>(role: KarniPrimitiveRole, Default: React.ForwardRefExoticComponent<React.PropsWithoutRef<P> & React.RefAttributes<T>>): React.ForwardRefExoticComponent<React.PropsWithoutRef<P> & React.RefAttributes<T>>;
|
|
76
|
+
/**
|
|
77
|
+
* Makes one part of Karni's select defer to a registered `select` family.
|
|
78
|
+
* @internal
|
|
79
|
+
*/
|
|
80
|
+
export declare function overridableSelectPart<K extends keyof KarniSelectParts, C extends KarniSelectParts[K]>(part: K, Default: C): C;
|
|
81
|
+
export {};
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* createKarniClient
|
|
2
|
+
* createKarniClient: the typed fetch wrapper every @karni/ui layer sits on
|
|
3
3
|
* (contract "Schema client" + D6).
|
|
4
4
|
*
|
|
5
5
|
* Responsibilities:
|
|
6
6
|
* - URL joining + query-string serialization (arrays become repeated keys,
|
|
7
7
|
* null/undefined are dropped),
|
|
8
|
-
* - JSON request/response handling
|
|
8
|
+
* - JSON request/response handling. A FormData body is passed through
|
|
9
9
|
* untouched with NO manual Content-Type (the D2 multipart rule),
|
|
10
10
|
* - DRF error passthrough: non-2xx responses throw KarniApiError carrying the
|
|
11
11
|
* parsed body (field errors / detail / non_field_errors) verbatim,
|
|
12
12
|
* - auth hook points per D6's client-owns-tokens design: the auth layer (not
|
|
13
13
|
* React) registers token injection, extra headers (session CSRF),
|
|
14
14
|
* credentials mode and a 401 handler via `setAuthHooks`. The client calls
|
|
15
|
-
* `onUnauthorized` at most ONCE per request and retries at most ONCE
|
|
16
|
-
* request that 401s again after a successful refresh is surfaced
|
|
15
|
+
* `onUnauthorized` at most ONCE per request and retries at most ONCE. A
|
|
16
|
+
* request that 401s again after a successful refresh is surfaced and never
|
|
17
17
|
* re-triggers refresh (the single-flight refresh promise itself lives in
|
|
18
18
|
* the auth layer behind this hook).
|
|
19
19
|
*/
|
|
@@ -27,7 +27,7 @@ export interface KarniClientOptions {
|
|
|
27
27
|
baseUrl: string;
|
|
28
28
|
/** Path of the OpenAPI document. Default "/api/schema/". */
|
|
29
29
|
schemaUrl?: string;
|
|
30
|
-
/** Initial access-token getter
|
|
30
|
+
/** Initial access-token getter. `setAuthHooks` can replace it later (D6). */
|
|
31
31
|
getAccessToken?: () => string | null;
|
|
32
32
|
/** Custom fetch implementation (tests, SSR). Resolved per request. */
|
|
33
33
|
fetch?: typeof globalThis.fetch;
|
|
@@ -50,10 +50,10 @@ export interface KarniRequestOptions {
|
|
|
50
50
|
/**
|
|
51
51
|
* Hook points the auth layer registers on the client (D6). Keys present in
|
|
52
52
|
* the object passed to `setAuthHooks` override the current value (an explicit
|
|
53
|
-
* `undefined` clears one)
|
|
53
|
+
* `undefined` clears one). Absent keys are kept.
|
|
54
54
|
*/
|
|
55
55
|
export interface KarniAuthHooks {
|
|
56
|
-
/** Called before every request
|
|
56
|
+
/** Called before every request. A non-null return is sent as Bearer. */
|
|
57
57
|
getAccessToken?: () => string | null;
|
|
58
58
|
/**
|
|
59
59
|
* Called once when a request gets a 401 (unless `skipAuthRefresh`). Return
|
|
@@ -63,10 +63,10 @@ export interface KarniAuthHooks {
|
|
|
63
63
|
onUnauthorized?: (error: KarniApiError) => Promise<boolean>;
|
|
64
64
|
/** Extra headers merged into every request (session-mode X-CSRFToken). */
|
|
65
65
|
getHeaders?: () => Record<string, string>;
|
|
66
|
-
/** fetch credentials mode
|
|
66
|
+
/** fetch credentials mode. Session mode sets "include". */
|
|
67
67
|
credentials?: RequestCredentials;
|
|
68
68
|
}
|
|
69
|
-
/** Thrown for non-2xx responses
|
|
69
|
+
/** Thrown for non-2xx responses, with the parsed DRF error body in `data`. */
|
|
70
70
|
export declare class KarniApiError extends Error {
|
|
71
71
|
readonly status: number;
|
|
72
72
|
/** Parsed body when JSON (field errors, detail, non_field_errors), else text. */
|
|
@@ -79,7 +79,7 @@ export interface KarniClient {
|
|
|
79
79
|
request<T>(path: string, options?: KarniRequestOptions): Promise<T>;
|
|
80
80
|
/**
|
|
81
81
|
* The same request (auth, query, 401 retry) returning the raw `Response`
|
|
82
|
-
* instead of a parsed body
|
|
82
|
+
* instead of a parsed body, for file downloads (`GET /<resource>/export/`).
|
|
83
83
|
* Non-2xx responses still throw `KarniApiError` with the parsed error body.
|
|
84
84
|
*/
|
|
85
85
|
requestRaw(path: string, options?: KarniRequestOptions): Promise<Response>;
|
|
@@ -74,12 +74,12 @@ export interface OpenApiDocument {
|
|
|
74
74
|
"x-karni-auth-mode"?: string;
|
|
75
75
|
[extension: string]: unknown;
|
|
76
76
|
}
|
|
77
|
-
/** "starts_at" → "Starts at"
|
|
77
|
+
/** "starts_at" → "Starts at", "is_public" → "Is public". */
|
|
78
78
|
export declare function humanize(name: string): string;
|
|
79
79
|
/**
|
|
80
80
|
* Resolves $ref chains and unwraps single-member allOf, merging siblings over
|
|
81
81
|
* the resolved target (so `{allOf: [$ref StatusEnum], readOnly: true}` yields
|
|
82
|
-
* the enum schema plus readOnly). Cycle-guarded
|
|
82
|
+
* the enum schema plus readOnly). Cycle-guarded: a cycle resolves to {}.
|
|
83
83
|
*/
|
|
84
84
|
export declare function resolveSchema(doc: OpenApiDocument, schema: SchemaObject | undefined, seen?: readonly string[]): SchemaObject | undefined;
|
|
85
85
|
/**
|
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
import { QueryClient } from '@tanstack/react-query';
|
|
2
2
|
import { ReactElement, ReactNode } from 'react';
|
|
3
|
+
import { KarniPrimitiveOverrides } from '../primitives/registry';
|
|
3
4
|
import { KarniClient } from './client';
|
|
4
5
|
export interface KarniProviderProps {
|
|
5
6
|
client: KarniClient;
|
|
6
|
-
/** Bring your own QueryClient
|
|
7
|
+
/** Bring your own QueryClient. One is created (once) when omitted. */
|
|
7
8
|
queryClient?: QueryClient;
|
|
9
|
+
/**
|
|
10
|
+
* The project's own primitives, by role: `{ button: Button, input: Input }`.
|
|
11
|
+
* Every Karni component then renders them in place of Karni's (see
|
|
12
|
+
* KarniPrimitivesProvider for the contract).
|
|
13
|
+
*/
|
|
14
|
+
primitives?: KarniPrimitiveOverrides;
|
|
8
15
|
children?: ReactNode;
|
|
9
16
|
}
|
|
10
|
-
/**
|
|
17
|
+
/** Rendered once at the app root, it provides the Karni client and a TanStack QueryClient to everything inside it. */
|
|
11
18
|
export declare function KarniProvider(props: KarniProviderProps): ReactElement;
|
|
12
|
-
/** Returns the Karni client from `KarniProvider
|
|
19
|
+
/** Returns the Karni client from `KarniProvider` and throws a descriptive error outside one. */
|
|
13
20
|
export declare function useKarniClient(): KarniClient;
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* The OpenAPI document served by the karni backend (drf-spectacular, OAS 3.0.3,
|
|
5
5
|
* COMPONENT_SPLIT_REQUEST=True per api contract B14) is parsed ONCE into these
|
|
6
|
-
* shapes
|
|
6
|
+
* shapes. Every generic component consumes FieldDef/ResourceSchema, never raw
|
|
7
7
|
* OpenAPI.
|
|
8
8
|
*/
|
|
9
9
|
export type FieldKind = "string" | "text" | "number" | "integer" | "boolean" | "date" | "datetime" | "enum" | "relation" | "file" | "json" | "array" | "unknown";
|
|
@@ -24,7 +24,7 @@ export interface FieldDef {
|
|
|
24
24
|
/** Flagged writeOnly OR present only in the request component (D1). */
|
|
25
25
|
writeOnly: boolean;
|
|
26
26
|
nullable: boolean;
|
|
27
|
-
/** Enum members
|
|
27
|
+
/** Enum members. BlankEnum's "" and NullEnum's null are never members (D1/D3). */
|
|
28
28
|
enumValues?: ReadonlyArray<string | number>;
|
|
29
29
|
/** Choice labels by value, from drf-spectacular's "* `value` - Label" description lines. */
|
|
30
30
|
enumLabels?: Readonly<Record<string, string>>;
|
|
@@ -40,23 +40,32 @@ export interface FieldDef {
|
|
|
40
40
|
relation?: RelationRef;
|
|
41
41
|
/**
|
|
42
42
|
* Underlying pk value type of a relation field, derived from the property's
|
|
43
|
-
* own emitted type/format
|
|
43
|
+
* own emitted type/format, never assumed (D3). Drives zodFor's relation
|
|
44
44
|
* value schema (uuid string vs integer).
|
|
45
45
|
*/
|
|
46
46
|
relationValueType?: "uuid" | "integer" | "string";
|
|
47
47
|
/** Declaration order in the schema (response props first, request-only appended). */
|
|
48
48
|
order: number;
|
|
49
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* An operation's `x-karni-access` rule as the backend publishes it: the access
|
|
52
|
+
* predicate governing the operation, e.g. `{"role": ["staff"]}`,
|
|
53
|
+
* `{"owned": "organizer"}`, `{"any": [...]}`. A display hint only: the server
|
|
54
|
+
* enforces it.
|
|
55
|
+
*/
|
|
56
|
+
export type KarniAccessRule = Readonly<Record<string, unknown>>;
|
|
50
57
|
export interface OperationDef {
|
|
51
58
|
path: string;
|
|
52
59
|
method: "get" | "post" | "put" | "patch" | "delete";
|
|
53
60
|
operationId?: string;
|
|
54
|
-
/** Declared query parameter names
|
|
61
|
+
/** Declared query parameter names. The filter bar derives from these (D5). */
|
|
55
62
|
queryParams: readonly string[];
|
|
56
63
|
/** Allowed expand keys when the expand param declares an enum (D5). */
|
|
57
64
|
expandEnum?: readonly string[];
|
|
58
65
|
/** True when the operation declares a `search` query parameter (api B14). */
|
|
59
66
|
hasSearch?: boolean;
|
|
67
|
+
/** The operation's `x-karni-access` rule, when the backend published one. */
|
|
68
|
+
access?: KarniAccessRule;
|
|
60
69
|
}
|
|
61
70
|
export interface ResourceSchema {
|
|
62
71
|
/** Router prefix, e.g. "events" (from x-karni-resource). */
|
|
@@ -67,18 +76,18 @@ export interface ResourceSchema {
|
|
|
67
76
|
create?: OperationDef;
|
|
68
77
|
update?: OperationDef;
|
|
69
78
|
remove?: OperationDef;
|
|
70
|
-
/** `GET /<resource>/export
|
|
79
|
+
/** `GET /<resource>/export/`: the list as a CSV/XLSX file (karni.transfer, P6). */
|
|
71
80
|
export?: OperationDef;
|
|
72
|
-
/** `POST /<resource>/import
|
|
81
|
+
/** `POST /<resource>/import/`: the import flow (upload, preview, commit). */
|
|
73
82
|
import?: OperationDef;
|
|
74
83
|
};
|
|
75
|
-
/** Ordered by declaration
|
|
84
|
+
/** Ordered by declaration. Unknown constructs land as kind "unknown". */
|
|
76
85
|
fields: readonly FieldDef[];
|
|
77
86
|
}
|
|
78
|
-
/** The whole parsed registry
|
|
87
|
+
/** The whole parsed registry, produced once per schema fetch by parseOpenApi. */
|
|
79
88
|
export interface ParsedSchema {
|
|
80
89
|
/** Keyed by router prefix (x-karni-resource). */
|
|
81
90
|
resources: Record<string, ResourceSchema>;
|
|
82
|
-
/** From the schema root's x-karni-auth-mode (api B14)
|
|
91
|
+
/** From the schema root's x-karni-auth-mode (api B14). Undefined when absent. */
|
|
83
92
|
authMode?: "jwt" | "session";
|
|
84
93
|
}
|
|
@@ -4,7 +4,7 @@ import { ParsedSchema, ResourceSchema } from './types';
|
|
|
4
4
|
export declare const SCHEMA_QUERY_KEY: readonly ["karni", "$schema"];
|
|
5
5
|
/**
|
|
6
6
|
* The whole parsed registry (resources + root x-karni-auth-mode). Internal to
|
|
7
|
-
* the package
|
|
7
|
+
* the package: the auth layer reads `data.authMode` from here. Apps use
|
|
8
8
|
* useSchema(resource).
|
|
9
9
|
*/
|
|
10
10
|
export declare function useParsedSchema(): UseQueryResult<ParsedSchema>;
|
|
@@ -12,6 +12,10 @@ export interface UseSchemaResult {
|
|
|
12
12
|
data: ResourceSchema | undefined;
|
|
13
13
|
isLoading: boolean;
|
|
14
14
|
error: unknown;
|
|
15
|
+
/** Fetches the schema document again: what a "Try again" after a failed load calls. */
|
|
16
|
+
refetch?: () => void;
|
|
17
|
+
/** True while the schema document is being fetched (the first load or a retry). */
|
|
18
|
+
isFetching?: boolean;
|
|
15
19
|
}
|
|
16
20
|
/** Returns one resource's parsed schema (fields, types, relations, operations) from the backend's OpenAPI document, fetched once per app. */
|
|
17
21
|
export declare function useSchema(resource: string): UseSchemaResult;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { KarniErrorContext } from './describe-error';
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
export interface ErrorStateProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "title"> {
|
|
4
|
+
/** What failed: a KarniApiError, a fetch failure, anything. */
|
|
5
|
+
error: unknown;
|
|
6
|
+
/** What the user was doing, so the message names it ("You can't delete events"). */
|
|
7
|
+
context?: KarniErrorContext;
|
|
8
|
+
/** Offered when trying again can succeed. */
|
|
9
|
+
onRetry?: () => void;
|
|
10
|
+
/** True while the retry runs: the button shows progress after ~500ms. */
|
|
11
|
+
retrying?: boolean;
|
|
12
|
+
/** More actions next to Retry (a sign-in link, "Back to the list"). */
|
|
13
|
+
actions?: React.ReactNode;
|
|
14
|
+
/** "panel" (a bordered block, the default) or "inline" (one compact row). */
|
|
15
|
+
variant?: "panel" | "inline";
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* What a component's `Error` slot receives: every Karni component that loads
|
|
19
|
+
* data takes `slots={{ Error: YourPanel }}` with these props.
|
|
20
|
+
*/
|
|
21
|
+
export type ErrorSlotProps = Pick<ErrorStateProps, "error" | "context" | "onRetry" | "retrying">;
|
|
22
|
+
/**
|
|
23
|
+
* What failed and what to do, in plain words (see describeError). Never shows
|
|
24
|
+
* a raw status code, but shows the server's own explanation when it gave one.
|
|
25
|
+
* Announced to screen readers when it appears.
|
|
26
|
+
*/
|
|
27
|
+
export declare const ErrorState: React.ForwardRefExoticComponent<ErrorStateProps & React.RefAttributes<HTMLDivElement>>;
|
|
28
|
+
export interface PermissionDeniedProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "title"> {
|
|
29
|
+
/** The action that is not allowed: "create", "delete", "export". */
|
|
30
|
+
action?: string;
|
|
31
|
+
/** What it applies to: "events", "this booking". */
|
|
32
|
+
resource?: string;
|
|
33
|
+
/** Replaces the default "Ask an administrator" sentence. */
|
|
34
|
+
description?: React.ReactNode;
|
|
35
|
+
/** A way forward ("Back to the list"). */
|
|
36
|
+
actions?: React.ReactNode;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Says, in plain words, which action the user's role does not allow, both
|
|
40
|
+
* on screens that know before asking the server (a role check) and after
|
|
41
|
+
* a 403.
|
|
42
|
+
*/
|
|
43
|
+
export declare const PermissionDenied: React.ForwardRefExoticComponent<PermissionDeniedProps & React.RefAttributes<HTMLDivElement>>;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { ButtonProps } from '../components/primitives/button';
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
export interface PendingButtonProps extends ButtonProps {
|
|
4
|
+
/** True while the action runs: the button is disabled and, after ~500ms, shows progress. */
|
|
5
|
+
pending?: boolean;
|
|
6
|
+
/** Announced to screen readers while pending ("Saving…"). */
|
|
7
|
+
pendingLabel?: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* A button for work that takes time. While `pending` it is disabled and
|
|
11
|
+
* `aria-busy`. Once the work is slow (500ms), a spinner covers the label,
|
|
12
|
+
* which stays in the layout, so the button never changes width or freezes
|
|
13
|
+
* silently. Renders the registered `button` primitive, so it follows the
|
|
14
|
+
* project's design system.
|
|
15
|
+
*/
|
|
16
|
+
export declare const PendingButton: React.ForwardRefExoticComponent<PendingButtonProps & React.RefAttributes<HTMLButtonElement>>;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export type KarniErrorKind = "offline" | "network" | "unauthenticated" | "forbidden" | "not-found" | "validation" | "conflict" | "rate-limited" | "timeout" | "server" | "unknown";
|
|
2
|
+
/** What the user was doing, so the message can name it ("You can't delete events."). */
|
|
3
|
+
export interface KarniErrorContext {
|
|
4
|
+
/** A verb: "view", "create", "edit", "delete", "export", "import", or your own. */
|
|
5
|
+
action?: string;
|
|
6
|
+
/** What it was done to, as the sentence needs it: "events", "this event". */
|
|
7
|
+
resource?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface KarniErrorDescription {
|
|
10
|
+
kind: KarniErrorKind;
|
|
11
|
+
/** A short headline in plain words. */
|
|
12
|
+
title: string;
|
|
13
|
+
/** What to do next. */
|
|
14
|
+
description: string;
|
|
15
|
+
/** Whether the same request can succeed if tried again. */
|
|
16
|
+
retryable: boolean;
|
|
17
|
+
/** The server's own explanation, when it gave one worth showing (never for 5xx). */
|
|
18
|
+
detail?: string;
|
|
19
|
+
/** The HTTP status, for logs and project code. Never shown to the user. */
|
|
20
|
+
status?: number;
|
|
21
|
+
}
|
|
22
|
+
/** Classifies `error` and words it for the person using the app. */
|
|
23
|
+
export declare function describeError(error: unknown, context?: KarniErrorContext): KarniErrorDescription;
|
|
24
|
+
/** `describeError(...).title`, capitalised, for one-line contexts (toasts, inline text). */
|
|
25
|
+
export declare function errorSummary(error: unknown, context?: KarniErrorContext): string;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** How long a request may run before Karni shows progress (ms). */
|
|
2
|
+
export declare const SLOW_AFTER_MS = 500;
|
|
3
|
+
/**
|
|
4
|
+
* True once `active` has stayed true for `delayMs` (default 500ms), false as
|
|
5
|
+
* soon as it turns false. Show progress only for slow work, so a fast request
|
|
6
|
+
* never flashes a spinner.
|
|
7
|
+
*/
|
|
8
|
+
export declare function useDelayedFlag(active: boolean, delayMs?: number): boolean;
|
|
9
|
+
/** Returns whether the browser reports a network connection and re-renders when it changes. */
|
|
10
|
+
export declare function useOnlineStatus(): boolean;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The theme's pre-paint script, with no React: shared by ThemeProvider and
|
|
3
|
+
* the `@karni/ui/vite` plugin, which runs in Node at build time.
|
|
4
|
+
*/
|
|
5
|
+
/** The localStorage key ThemeProvider keeps the user's choice under. */
|
|
6
|
+
export declare const THEME_STORAGE_KEY = "karni-theme";
|
|
7
|
+
/**
|
|
8
|
+
* Inline script for index.html's `<head>`: applies the stored (or system) theme
|
|
9
|
+
* before the first paint. Uses the default storage key. In a Vite app,
|
|
10
|
+
* `karniTheme()` from `@karni/ui/vite` puts it there.
|
|
11
|
+
*/
|
|
12
|
+
export declare const KARNI_THEME_SCRIPT = "(function(){try{var t=localStorage.getItem(\"karni-theme\");var d=t===\"dark\"||((t===null||t===\"system\")&&matchMedia(\"(prefers-color-scheme: dark)\").matches);document.documentElement.classList.toggle(\"dark\",d);document.documentElement.style.colorScheme=d?\"dark\":\"light\"}catch(e){}})();";
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Light, dark or the system's choice, for the whole app.
|
|
3
|
+
*
|
|
4
|
+
* <ThemeProvider> // follows the system until the user picks
|
|
5
|
+
* <App /> // <ThemeToggle /> anywhere lets them pick
|
|
6
|
+
* </ThemeProvider>
|
|
7
|
+
*
|
|
8
|
+
* The choice is kept per browser (localStorage) and applied as the `dark`
|
|
9
|
+
* class on <html>, which is what @karni/ui's tokens switch on. Put
|
|
10
|
+
* KARNI_THEME_SCRIPT in index.html's <head> (in a Vite app, `karniTheme()`
|
|
11
|
+
* from `@karni/ui/vite` does) so the first paint already has the right
|
|
12
|
+
* theme (no light flash before React starts).
|
|
13
|
+
*/
|
|
14
|
+
import * as React from "react";
|
|
15
|
+
export type ThemePreference = "light" | "dark" | "system";
|
|
16
|
+
export type ResolvedTheme = "light" | "dark";
|
|
17
|
+
export { KARNI_THEME_SCRIPT, THEME_STORAGE_KEY } from './script';
|
|
18
|
+
interface ThemeContextValue {
|
|
19
|
+
/** What the user chose ("system" follows the operating system). */
|
|
20
|
+
theme: ThemePreference;
|
|
21
|
+
/** What is applied right now. */
|
|
22
|
+
resolvedTheme: ResolvedTheme;
|
|
23
|
+
setTheme: (theme: ThemePreference) => void;
|
|
24
|
+
}
|
|
25
|
+
export interface ThemeProviderProps {
|
|
26
|
+
/** Used until the user chooses, "system" by default. */
|
|
27
|
+
defaultTheme?: ThemePreference;
|
|
28
|
+
/** localStorage key for the user's choice. Change it to keep apps on one origin apart. */
|
|
29
|
+
storageKey?: string;
|
|
30
|
+
/** The element that gets the `dark` class, <html> by default. */
|
|
31
|
+
target?: () => HTMLElement;
|
|
32
|
+
children?: React.ReactNode;
|
|
33
|
+
}
|
|
34
|
+
/** Rendered once around the app, it applies the user's theme choice and keeps it. */
|
|
35
|
+
export declare function ThemeProvider({ defaultTheme, storageKey, target, children, }: ThemeProviderProps): React.ReactElement;
|
|
36
|
+
/** Returns the theme choice and a setter, and throws a descriptive error outside ThemeProvider. */
|
|
37
|
+
export declare function useTheme(): ThemeContextValue;
|
|
38
|
+
export interface ThemeToggleProps {
|
|
39
|
+
className?: string;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* One button that cycles Light → Dark → System. Its accessible name says the
|
|
43
|
+
* current choice and the next one ("Theme: Dark. Switch to System").
|
|
44
|
+
*/
|
|
45
|
+
export declare function ThemeToggle({ className }: ThemeToggleProps): React.ReactElement;
|