@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.
Files changed (127) hide show
  1. package/README.md +712 -72
  2. package/dist/index.cjs +5 -5
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.js +8660 -4725
  5. package/dist/index.js.map +1 -1
  6. package/dist/manifest.json +18 -1
  7. package/dist/script-BVJ9BbPm.cjs +2 -0
  8. package/dist/script-BVJ9BbPm.cjs.map +1 -0
  9. package/dist/script-D1fn4Dm4.js +6 -0
  10. package/dist/script-D1fn4Dm4.js.map +1 -0
  11. package/dist/src/auth/AuthGuard.d.ts +69 -4
  12. package/dist/src/auth/LoginForm.d.ts +41 -7
  13. package/dist/src/auth/engine.d.ts +15 -13
  14. package/dist/src/auth/provider.d.ts +10 -10
  15. package/dist/src/auth/storage.d.ts +1 -1
  16. package/dist/src/auth/types.d.ts +3 -3
  17. package/dist/src/components/brand/karni-mark.d.ts +4 -4
  18. package/dist/src/components/chart/data-chart.d.ts +66 -11
  19. package/dist/src/components/empty-state/empty-state.d.ts +22 -2
  20. package/dist/src/components/file-upload/FileUpload.d.ts +67 -3
  21. package/dist/src/components/layout/layout.d.ts +104 -9
  22. package/dist/src/components/layout/route-focus.d.ts +43 -0
  23. package/dist/src/components/modal/modal.d.ts +49 -8
  24. package/dist/src/components/page/page.d.ts +36 -5
  25. package/dist/src/components/primitives/button.d.ts +1 -0
  26. package/dist/src/components/primitives/checkbox.d.ts +4 -1
  27. package/dist/src/components/primitives/dropdown-menu.d.ts +1 -1
  28. package/dist/src/components/primitives/icons.d.ts +9 -1
  29. package/dist/src/components/primitives/input.d.ts +4 -1
  30. package/dist/src/components/primitives/label.d.ts +4 -1
  31. package/dist/src/components/primitives/select.d.ts +37 -6
  32. package/dist/src/components/primitives/skeleton.d.ts +4 -1
  33. package/dist/src/components/primitives/switch.d.ts +4 -1
  34. package/dist/src/components/primitives/tabs.d.ts +2 -2
  35. package/dist/src/components/primitives/textarea.d.ts +4 -1
  36. package/dist/src/components/resource/ResourceDetail.d.ts +3 -3
  37. package/dist/src/components/resource/ResourceForm.d.ts +4 -3
  38. package/dist/src/components/resource/ResourceSelect.d.ts +3 -1
  39. package/dist/src/components/resource/ResourceTable.d.ts +9 -3
  40. package/dist/src/components/resource/audit-fields.d.ts +1 -1
  41. package/dist/src/components/resource/detail/ResourceDetail.d.ts +109 -11
  42. package/dist/src/components/resource/empty-value.d.ts +17 -0
  43. package/dist/src/components/resource/form/ResourceForm.d.ts +65 -6
  44. package/dist/src/components/resource/form/field-inputs.d.ts +15 -1
  45. package/dist/src/components/resource/form/form-states.d.ts +87 -0
  46. package/dist/src/components/resource/form/prepare.d.ts +3 -3
  47. package/dist/src/components/resource/form/server-errors.d.ts +14 -0
  48. package/dist/src/components/resource/history/ChangeHistory.d.ts +46 -4
  49. package/dist/src/components/resource/import/ResourceImport.d.ts +73 -7
  50. package/dist/src/components/resource/nouns.d.ts +12 -0
  51. package/dist/src/components/resource/query-state.d.ts +20 -0
  52. package/dist/src/components/resource/resource-names.d.ts +23 -0
  53. package/dist/src/components/resource/select/ResourceSelect.d.ts +39 -8
  54. package/dist/src/components/resource/select/select-states.d.ts +68 -0
  55. package/dist/src/components/resource/select/use-option-pages.d.ts +37 -0
  56. package/dist/src/components/resource/select/venue-backend.d.ts +144 -0
  57. package/dist/src/components/resource/table/BulkActionsBar.d.ts +62 -5
  58. package/dist/src/components/resource/table/CardList.d.ts +31 -0
  59. package/dist/src/components/resource/table/ConfirmDialog.d.ts +28 -0
  60. package/dist/src/components/resource/table/ExportButtons.d.ts +2 -2
  61. package/dist/src/components/resource/table/FilterBar.d.ts +3 -3
  62. package/dist/src/components/resource/table/PaginationControls.d.ts +15 -3
  63. package/dist/src/components/resource/table/ResourceTable.d.ts +79 -14
  64. package/dist/src/components/resource/table/RowCard.d.ts +31 -0
  65. package/dist/src/components/resource/table/TableView.d.ts +29 -0
  66. package/dist/src/components/resource/table/ViewOptions.d.ts +19 -0
  67. package/dist/src/components/resource/table/access.d.ts +12 -0
  68. package/dist/src/components/resource/table/derive-columns.d.ts +2 -2
  69. package/dist/src/components/resource/table/grid-navigation.d.ts +23 -0
  70. package/dist/src/components/resource/table/layout.d.ts +37 -0
  71. package/dist/src/components/resource/table/preferences.d.ts +12 -0
  72. package/dist/src/components/resource/table/slots.d.ts +41 -0
  73. package/dist/src/components/resource/table/states.d.ts +91 -0
  74. package/dist/src/components/resource/table/story-fixtures.d.ts +25 -0
  75. package/dist/src/components/resource/table/structural-columns.d.ts +25 -0
  76. package/dist/src/components/resource/table/table-icons.d.ts +16 -0
  77. package/dist/src/components/resource/table/table-utils.d.ts +27 -3
  78. package/dist/src/components/resource/table/url-state.d.ts +67 -0
  79. package/dist/src/components/resource/table/use-container-width.d.ts +10 -0
  80. package/dist/src/components/resource/testapp-fixtures.d.ts +78 -0
  81. package/dist/src/components/stat/stat.d.ts +36 -6
  82. package/dist/src/components/toast/toast-store.d.ts +1 -1
  83. package/dist/src/components/toast/toaster.d.ts +39 -6
  84. package/dist/src/components/toast/use-toast.d.ts +1 -1
  85. package/dist/src/hooks/keys.d.ts +4 -4
  86. package/dist/src/hooks/multipart.d.ts +9 -9
  87. package/dist/src/hooks/params.d.ts +1 -1
  88. package/dist/src/hooks/types.d.ts +3 -3
  89. package/dist/src/hooks/useChangeHistory.d.ts +3 -2
  90. package/dist/src/hooks/useResourceImport.d.ts +2 -2
  91. package/dist/src/hooks/useResourceMutations.d.ts +2 -2
  92. package/dist/src/hooks/useUnsavedChangesGuard.d.ts +31 -0
  93. package/dist/src/index.d.ts +49 -31
  94. package/dist/src/lib/roles.d.ts +4 -4
  95. package/dist/src/lib/variants.d.ts +1 -1
  96. package/dist/src/primitives/registry.d.ts +81 -0
  97. package/dist/src/schema/client.d.ts +10 -10
  98. package/dist/src/schema/parse.d.ts +2 -2
  99. package/dist/src/schema/provider.d.ts +10 -3
  100. package/dist/src/schema/types.d.ts +18 -9
  101. package/dist/src/schema/useSchema.d.ts +5 -1
  102. package/dist/src/states/ErrorState.d.ts +43 -0
  103. package/dist/src/states/PendingButton.d.ts +16 -0
  104. package/dist/src/states/describe-error.d.ts +25 -0
  105. package/dist/src/states/hooks.d.ts +10 -0
  106. package/dist/src/theme/script.d.ts +12 -0
  107. package/dist/src/theme/theme.d.ts +45 -0
  108. package/dist/styles.css +174 -13
  109. package/dist/tailwind-preset.cjs +2 -2
  110. package/dist/tailwind-preset.cjs.map +1 -1
  111. package/dist/tailwind-preset.d.ts +83 -1
  112. package/dist/tailwind-preset.js +181 -62
  113. package/dist/tailwind-preset.js.map +1 -1
  114. package/dist/tailwind-v4.css +31 -0
  115. package/dist/vite-plugin.cjs +2 -0
  116. package/dist/vite-plugin.cjs.map +1 -0
  117. package/dist/vite-plugin.d.ts +19 -0
  118. package/dist/vite-plugin.js +16 -0
  119. package/dist/vite-plugin.js.map +1 -0
  120. package/eslint-plugin/index.js +4 -0
  121. package/eslint-plugin/rules/max-page-lines.js +1 -1
  122. package/eslint-plugin/rules/no-deep-imports.js +1 -1
  123. package/eslint-plugin/rules/no-direct-http.js +1 -1
  124. package/eslint-plugin/rules/no-raw-elements.js +1 -1
  125. package/eslint-plugin/rules/no-style-literals.js +222 -0
  126. package/eslint-plugin/rules.json +10 -4
  127. package/package.json +22 -19
@@ -1,16 +1,24 @@
1
1
  import { ColumnDef, SortingState } from '@tanstack/react-table';
2
- import { KarniQueryValue } from '../../../schema/client';
3
2
  import { ExportFormat } from '../../../hooks/useResourceImport';
4
- import { BulkAction } from './BulkActionsBar';
3
+ import { KarniQueryValue } from '../../../schema/client';
4
+ import { BulkAction, BulkResult } from './BulkActionsBar';
5
5
  import { ResourceColumn } from './derive-columns';
6
+ import { ResourceTableDensity } from './preferences';
7
+ import { ResourceTableSlots } from './slots';
8
+ import { ResourceTableKeyboardNavigation } from './TableView';
9
+ import { ResourceTableStateStore } from './url-state';
6
10
  import * as React from "react";
7
- export type { BulkAction, ResourceColumn };
11
+ export type { BulkAction, BulkResult, ResourceColumn, ResourceTableDensity, ResourceTableKeyboardNavigation, };
8
12
  export declare const DEFAULT_PAGE_SIZE = 25;
13
+ /** The page sizes offered by default (`pageSizeOptions`). */
14
+ export declare const DEFAULT_PAGE_SIZE_OPTIONS: readonly number[];
15
+ /** Below this container width (px) rows render as cards (`cardBreakpoint`). */
16
+ export declare const DEFAULT_CARD_BREAKPOINT = 640;
9
17
  export interface ResourceTableProps<T = Record<string, unknown>> {
10
18
  resource: string;
11
19
  /**
12
20
  * 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.
21
+ * as `created_at`, or a json/file field) shows it. `hidden: true` hides one.
14
22
  */
15
23
  columns?: ReadonlyArray<ResourceColumn<T>>;
16
24
  /**
@@ -26,46 +34,103 @@ export interface ResourceTableProps<T = Record<string, unknown>> {
26
34
  rowActions?: (row: T) => React.ReactNode;
27
35
  /** Passing this enables the default edit row action. */
28
36
  onEdit?: (row: T) => void;
29
- /** Enables the default delete row action (uses useResourceMutations). */
37
+ /** Enables the default delete row action (confirms in a dialog, uses useResourceMutations). */
30
38
  enableDelete?: boolean;
31
39
  bulkActions?: ReadonlyArray<BulkAction>;
40
+ /**
41
+ * Opens a row. The whole row is clickable, and its first cell is a button,
42
+ * so the row opens from the keyboard too (Enter on any cell of the grid).
43
+ */
32
44
  onRowClick?: (row: T) => void;
33
45
  /**
34
46
  * `expand` query values. Default: single-relation field names ∩ the list
35
- * operation's expand enum — never guessed beyond the schema (D5).
47
+ * operation's expand enum, never guessed beyond the schema (D5).
36
48
  */
37
49
  expand?: readonly string[];
50
+ /** The ordering when the URL names none. */
38
51
  initialSorting?: SortingState;
39
52
  /** Sent as `page_size`. Default 25. */
40
53
  pageSize?: number;
54
+ /** The page sizes a user can choose. Default `[10, 25, 50, 100]`. `pageSize` is always offered. */
55
+ pageSizeOptions?: readonly number[];
41
56
  /**
42
- * Field names to build the filter bar from; `false` disables it. Default:
57
+ * Field names to build the filter bar from. `false` disables it. Default:
43
58
  * (enum/boolean/relation fields) ∩ declared list query params, domain
44
59
  * fields first, audit fields only with `showAuditFields`, cap 4 (D5).
45
60
  * Only enum, boolean and relation fields get controls.
46
61
  */
47
62
  filters?: readonly string[] | false;
48
63
  /**
49
- * Fixed filters sent with every request and hidden from the filter bar —
64
+ * Fixed filters sent with every request and hidden from the filter bar,
50
65
  * e.g. `{ event: eventId }` for "the bookings of this event". A user filter
51
- * on the same field never overrides them.
66
+ * on the same field never overrides them. Never written to the URL.
52
67
  */
53
68
  params?: Readonly<Record<string, KarniQueryValue>>;
54
69
  /**
55
70
  * 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.
71
+ * `GET /<resource>/export/`, as every Karni BaseViewSet does. The file
72
+ * carries the table's current filters, search and ordering. The backend
73
+ * applies the row scope and field visibility. Default `["csv", "xlsx"]`.
74
+ * `false` hides them.
59
75
  */
60
76
  exportFormats?: readonly ExportFormat[] | false;
61
77
  /** Extra toolbar content, rendered to the right of the search/filter row. */
62
78
  toolbar?: React.ReactNode;
63
- /** Replaces the default EmptyState when the list has no results. */
79
+ /**
80
+ * Replaces both empty screens. Prefer `slots.NoRecords` and
81
+ * `slots.NoResults`, which tell "nothing yet" from "nothing matches".
82
+ */
64
83
  empty?: React.ReactNode;
65
84
  /** Rendered under the pagination row. */
66
85
  footer?: React.ReactNode;
67
- /** Overrides displayLabel() for relation cells (and the delete confirm). */
86
+ /** Overrides displayLabel() for relation cells, row names (checkboxes, cards, the delete question). */
68
87
  getRowLabel?: (row: Record<string, unknown>) => string;
88
+ /** Replace any state screen or part (see ResourceTableSlots). */
89
+ slots?: Partial<ResourceTableSlots>;
90
+ /** The resource in sentences, plural ("events"). Default: from the resource name. */
91
+ resourceLabel?: string;
92
+ /** One of them ("event"). Default: a singular of `resourceLabel`. */
93
+ itemLabel?: string;
94
+ /** Names the table for assistive technology. Default: `resourceLabel`, capitalised. */
95
+ caption?: string;
96
+ /** Offers a create button (toolbar and the "no records yet" screen). */
97
+ onCreate?: () => void;
98
+ /** Default "New <itemLabel>". */
99
+ createLabel?: string;
100
+ /**
101
+ * Whether the role may create. Default: the schema documents a create
102
+ * operation and its access rule does not refuse this user.
103
+ */
104
+ canCreate?: boolean;
105
+ /** Whether a row's edit action shows. Default: the update operation's access rule for this user and row. */
106
+ canEdit?: boolean | ((row: T) => boolean);
107
+ /** Whether a row's delete action shows. Default: the delete operation's access rule for this user and row. */
108
+ canDelete?: boolean | ((row: T) => boolean);
109
+ /** Keep page, page size, sort, search and filters in the URL. Default true. */
110
+ syncUrl?: boolean;
111
+ /** Prefix of this table's URL keys (`events.page=2`). Default: the resource name, `""` for none. */
112
+ urlNamespace?: string;
113
+ /** Where URL state lives. Default `window.history`. Plug a router in here. */
114
+ stateStore?: ResourceTableStateStore;
115
+ /** Row density, controlled. Uncontrolled, the user's choice is remembered per user. */
116
+ density?: ResourceTableDensity;
117
+ /** The density before the user chooses. Default "comfortable". */
118
+ defaultDensity?: ResourceTableDensity;
119
+ onDensityChange?: (density: ResourceTableDensity) => void;
120
+ /** The "View" menu (density, column visibility). Default true. */
121
+ viewOptions?: boolean;
122
+ /** Below this container width (px) rows render as cards. Default 640, and 0 means never. */
123
+ cardBreakpoint?: number;
124
+ /**
125
+ * How the keyboard moves through the table layout. Default `"grid"`, the
126
+ * WAI-ARIA grid pattern: the whole table is one Tab stop, the arrow keys
127
+ * move between rows and cells (each control of a cell is a stop of its
128
+ * own), Home and End go to the ends of a row, Ctrl+Home and Ctrl+End to
129
+ * the first and last cell, Enter on a cell opens the row (`onRowClick`) and
130
+ * Space on a cell selects it (`bulkActions`). `"native"` is a plain table
131
+ * where every checkbox, button and link is its own Tab stop, as before.
132
+ */
133
+ keyboardNavigation?: ResourceTableKeyboardNavigation;
69
134
  className?: string;
70
135
  }
71
136
  /** Schema-driven data table for one resource: columns, sorting, filters, search, pagination, row actions and bulk select, all derived from the OpenAPI schema. */
@@ -0,0 +1,31 @@
1
+ import { ResourceTableDensity } from './preferences';
2
+ /**
3
+ * One row as a card, for containers narrower than ResourceTable's
4
+ * `cardBreakpoint`: the title (the first column, or `getRowLabel`), then a
5
+ * label: value pair per visible column, with the row's checkbox and actions.
6
+ * The default of ResourceTable's `RowCard` slot.
7
+ */
8
+ import * as React from "react";
9
+ export interface ResourceTableCardField {
10
+ id: string;
11
+ label: React.ReactNode;
12
+ value: React.ReactNode;
13
+ }
14
+ export interface ResourceTableRowCardProps<T = Record<string, unknown>> {
15
+ row: T;
16
+ /** The card's heading: the first column's cell, or `getRowLabel(row)`. */
17
+ title: React.ReactNode;
18
+ /** The other visible columns, as label: value pairs. */
19
+ fields: readonly ResourceTableCardField[];
20
+ /** The row's checkbox, when the table has bulk actions. */
21
+ selection?: React.ReactNode;
22
+ /** The row's actions (edit, delete, `rowActions`). */
23
+ actions?: React.ReactNode;
24
+ /** Opens the row (`onRowClick`). The title becomes a button. */
25
+ onOpen?: () => void;
26
+ selected: boolean;
27
+ /** Why the last bulk action failed for this row. */
28
+ failure?: string;
29
+ density: ResourceTableDensity;
30
+ }
31
+ export declare function RowCard<T>(props: ResourceTableRowCardProps<T>): React.ReactElement;
@@ -0,0 +1,29 @@
1
+ import { Header, Table } from '@tanstack/react-table';
2
+ import { ResourceTableDensity } from './preferences';
3
+ import * as React from "react";
4
+ /** How the keyboard moves through the table's rows (ResourceTable's `keyboardNavigation`). */
5
+ export type ResourceTableKeyboardNavigation = "grid" | "native";
6
+ export interface TableViewProps<T> {
7
+ table: Table<T>;
8
+ /** Names the table for assistive technology ("Events"). */
9
+ caption: string;
10
+ density: ResourceTableDensity;
11
+ /** A sort or page change is loading: sort buttons ignore clicks. */
12
+ busy: boolean;
13
+ /** The column whose header was clicked last, for its progress spinner. */
14
+ sortTarget: string | null;
15
+ /** Work has run for 500ms: show progress. */
16
+ slow: boolean;
17
+ onSort: (header: Header<T, unknown>, event: React.MouseEvent) => void;
18
+ onRowClick?: (row: T) => void;
19
+ /** Why the last bulk action failed, by row id. */
20
+ failures: Readonly<Record<string, string>>;
21
+ /** 1-based position of the first row in the whole list, for aria-rowindex. */
22
+ firstRowIndex: number;
23
+ totalCount?: number;
24
+ /** The rows are stale (a later request failed or is slow): dim them. */
25
+ dimmed: boolean;
26
+ scrollRef: React.RefObject<HTMLDivElement>;
27
+ keyboardNavigation: ResourceTableKeyboardNavigation;
28
+ }
29
+ export declare function TableView<T>(props: TableViewProps<T>): React.ReactElement;
@@ -0,0 +1,19 @@
1
+ import { ResourceTableDensity } from './preferences';
2
+ /**
3
+ * ResourceTable's "View" menu: compact rows on or off, and which columns
4
+ * show. Both are remembered per user (see ./preferences). A Radix dropdown,
5
+ * so it is operable by keyboard (arrows, Space to toggle, Escape to close).
6
+ * Internal to @karni/ui. ResourceTable's `viewOptions={false}` hides it.
7
+ */
8
+ import * as React from "react";
9
+ export interface ViewOptionsProps {
10
+ density: ResourceTableDensity;
11
+ onDensityChange: (density: ResourceTableDensity) => void;
12
+ columns: ReadonlyArray<{
13
+ id: string;
14
+ label: string;
15
+ visible: boolean;
16
+ }>;
17
+ onColumnVisibilityChange: (id: string, visible: boolean) => void;
18
+ }
19
+ export declare function ViewOptions(props: ViewOptionsProps): React.ReactElement;
@@ -0,0 +1,12 @@
1
+ import { KarniUser } from '../../../auth/types';
2
+ import { KarniAccessRule } from '../../../schema/types';
3
+ /** Who is asking. `null` when the table renders outside an AuthProvider. */
4
+ export interface AccessIdentity {
5
+ status: "loading" | "authenticated" | "anonymous";
6
+ user: KarniUser | null;
7
+ }
8
+ /**
9
+ * `true` when the rule allows the operation for this identity (and row),
10
+ * `false` when it refuses, `undefined` when only the server can tell.
11
+ */
12
+ export declare function evaluateAccess(rule: KarniAccessRule | undefined, identity: AccessIdentity | null, row?: unknown): boolean | undefined;
@@ -25,9 +25,9 @@ export interface DeriveColumnsArgs<T> {
25
25
  /**
26
26
  * Builds the derived column set: domain columns (schema order), then any
27
27
  * shown audit columns (AUDIT_FIELDS order), then `columns` entries naming
28
- * fields the schema does not declare — plain accessor columns (escape hatch
28
+ * fields the schema does not declare, as plain accessor columns (escape hatch
29
29
  * for computed or server-extra keys). `columns` overrides merge onto their
30
- * fields; naming a default-skipped field (json, audit, ...) shows it.
30
+ * fields, and naming a default-skipped field (json, audit, ...) shows it.
31
31
  */
32
32
  export declare function deriveColumns<T>(args: DeriveColumnsArgs<T>): Array<ColumnDef<T>>;
33
33
  export {};
@@ -0,0 +1,23 @@
1
+ /**
2
+ * ResourceTable's keyboard model in the table layout: the WAI-ARIA grid
3
+ * pattern with a roving tabindex. The whole table is one Tab stop. The arrow
4
+ * keys move between rows and cells, Home and End to the ends of a row, and
5
+ * Ctrl+Home and Ctrl+End to the first and last cell of the grid.
6
+ *
7
+ * The stops are read from the DOM, so any cell content works: a cell that
8
+ * holds controls (a checkbox, the row's button, links, the row actions, a
9
+ * menu trigger) puts each control in the row's arrow order, and a cell
10
+ * without one is a stop itself. Exactly one stop has tabindex 0 (the last
11
+ * one focused). Every other stop has -1, and a MutationObserver keeps it so
12
+ * when rows, pages or cell content change. Internal to @karni/ui.
13
+ */
14
+ import * as React from "react";
15
+ export interface GridNavigation {
16
+ onKeyDown: (event: React.KeyboardEvent<HTMLTableElement>) => void;
17
+ onFocus: (event: React.FocusEvent<HTMLTableElement>) => void;
18
+ }
19
+ /**
20
+ * The roving tabindex of a table. Returns the handlers for the <table>. When
21
+ * `enabled` is false they do nothing and the table is left as rendered.
22
+ */
23
+ export declare function useGridNavigation(tableRef: React.RefObject<HTMLTableElement>, enabled: boolean): GridNavigation;
@@ -0,0 +1,37 @@
1
+ import { ResourceTableDensity } from './preferences';
2
+ /** Row and header geometry per density. Every value is a spacing token. */
3
+ export declare const DENSITY_CLASSES: Record<ResourceTableDensity, {
4
+ head: string;
5
+ cell: string;
6
+ action: string;
7
+ card: string;
8
+ }>;
9
+ /**
10
+ * The scrolling box: both directions scroll inside it, so 50 columns never
11
+ * push the page sideways and the header can stick. Its height is capped by
12
+ * the `--karni-table-max-height` token (70vh). Set it to `none` to let the
13
+ * page scroll instead.
14
+ */
15
+ export declare const SCROLL_BOX = "group/scroll relative w-full max-h-[var(--karni-table-max-height)] overflow-auto rounded-md border border-border bg-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring";
16
+ /**
17
+ * The scroll box of a grid: a cell or control the keyboard focuses scrolls
18
+ * into view below the sticky header, not under it.
19
+ */
20
+ export declare const GRID_SCROLL_BOX = "scroll-pt-10";
21
+ /** A grid cell without a control takes focus itself, and shows it inside its edges. */
22
+ export declare const GRID_CELL = "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring";
23
+ export declare const TABLE = "w-full border-separate border-spacing-0 text-sm";
24
+ /** Sticky on vertical scroll, above the frozen body cells. */
25
+ export declare const THEAD = "sticky top-0 z-raised";
26
+ export declare const HEAD_CELL = "whitespace-nowrap border-b border-border bg-muted text-left align-middle text-xs font-medium text-muted-foreground";
27
+ /** Opaque backgrounds on every cell, so frozen cells hide what scrolls beneath them. */
28
+ export declare const BODY_CELL = "whitespace-nowrap border-b border-border bg-background align-middle group-hover/row:bg-muted group-data-[state=selected]/row:bg-muted";
29
+ export declare const LAST_ROW = "[&>tr:last-child>td]:border-b-0";
30
+ /** The checkbox column: a fixed width, so the frozen first column can sit after it. */
31
+ export declare const SELECT_CELL = "sticky left-0 w-10 min-w-10 px-3";
32
+ /** The frozen first data column, with an edge once the table scrolls sideways. */
33
+ export declare const FROZEN_CELL = "sticky border-r border-r-transparent group-data-[scrolled=true]/scroll:border-r-border";
34
+ export declare const FROZEN_AT_START = "left-0";
35
+ export declare const FROZEN_AFTER_SELECT = "left-10";
36
+ /** The frozen column's content is capped, so it never takes the whole width. */
37
+ export declare const FROZEN_CONTENT = "max-w-72 truncate";
@@ -0,0 +1,12 @@
1
+ export type ResourceTableDensity = "comfortable" | "compact";
2
+ export declare function densityKey(userId: string | number | undefined): string;
3
+ export declare function hiddenColumnsKey(namespace: string, userId: string | number | undefined): string;
4
+ /**
5
+ * A value remembered under `key`, re-read when the key changes (another user
6
+ * signs in). `parse` returns undefined for anything it does not recognise.
7
+ */
8
+ export declare function useStoredPreference<V>(key: string, fallback: V, parse: (raw: string) => V | undefined, serialize: (value: V) => string): [V, (value: V) => void];
9
+ export declare function parseDensity(raw: string): ResourceTableDensity | undefined;
10
+ export declare function parseColumnList(raw: string): readonly string[] | undefined;
11
+ export declare const asString: (value: string) => string;
12
+ export declare const serializeColumnList: (value: readonly string[]) => string;
@@ -0,0 +1,41 @@
1
+ import { ErrorSlotProps } from '../../../states/ErrorState';
2
+ import { BulkActionsBarProps } from './BulkActionsBar';
3
+ import { PaginationControlsProps } from './PaginationControls';
4
+ import { ResourceTableRowCardProps } from './RowCard';
5
+ import { ResourceTableForbiddenProps, ResourceTableLoadingProps, ResourceTableNoRecordsProps, ResourceTableNoResultsProps, ResourceTableOfflineProps } from './states';
6
+ /**
7
+ * ResourceTable's slots: every state screen and structural part a project
8
+ * can replace with its own component, `slots={{ NoRecords: OurEmpty }}`.
9
+ * Each slot receives the props type exported next to it. The defaults are
10
+ * exported as `defaultResourceTableSlots`, so a replacement can wrap one
11
+ * (`(props) => <defaultResourceTableSlots.Error {...props} />`).
12
+ */
13
+ import type * as React from "react";
14
+ /** The pagination row's props (the `Pagination` slot). */
15
+ export type ResourceTablePaginationProps = PaginationControlsProps;
16
+ /** The bulk actions bar's props (the `BulkActions` slot). */
17
+ export type ResourceTableBulkActionsProps = BulkActionsBarProps;
18
+ export interface ResourceTableSlots {
19
+ /** The first load: the layout in outline. */
20
+ Loading: React.ComponentType<ResourceTableLoadingProps>;
21
+ /** The resource has no rows at all yet. */
22
+ NoRecords: React.ComponentType<ResourceTableNoRecordsProps>;
23
+ /** Rows exist, but none match the search and filters. */
24
+ NoResults: React.ComponentType<ResourceTableNoResultsProps>;
25
+ /** The list (or the schema) failed to load and there is nothing to show. */
26
+ Error: React.ComponentType<ErrorSlotProps>;
27
+ /** A refresh failed while rows were already on screen: the rows stay. */
28
+ RefetchError: React.ComponentType<ErrorSlotProps>;
29
+ /** The role may not view this resource (the list answered 403). */
30
+ Forbidden: React.ComponentType<ResourceTableForbiddenProps>;
31
+ /** The browser is offline. */
32
+ Offline: React.ComponentType<ResourceTableOfflineProps>;
33
+ /** The pagination row. */
34
+ Pagination: React.ComponentType<ResourceTablePaginationProps>;
35
+ /** The selected-rows bar. */
36
+ BulkActions: React.ComponentType<ResourceTableBulkActionsProps>;
37
+ /** One row as a card, below `cardBreakpoint`. */
38
+ RowCard: React.ComponentType<ResourceTableRowCardProps<unknown>>;
39
+ }
40
+ export declare const defaultResourceTableSlots: ResourceTableSlots;
41
+ export type { ResourceTableForbiddenProps, ResourceTableLoadingProps, ResourceTableNoRecordsProps, ResourceTableNoResultsProps, ResourceTableOfflineProps, ResourceTableRowCardProps, };
@@ -0,0 +1,91 @@
1
+ import { ErrorSlotProps } from '../../../states/ErrorState';
2
+ import { ResourceTableDensity } from './preferences';
3
+ /**
4
+ * ResourceTable's state screens, each the default of one slot: the loading
5
+ * skeleton, the two empty screens (nothing yet / nothing matches), the error
6
+ * panels, the permission screen and the offline notice. A project replaces
7
+ * any of them with `slots={{ NoRecords: OurEmpty }}`, and each receives the
8
+ * props documented here.
9
+ */
10
+ import * as React from "react";
11
+ /** What the Loading slot receives: enough to draw the real layout's outline. */
12
+ export interface ResourceTableLoadingProps {
13
+ /** Columns of the real table, the checkbox and actions columns included (a guess until the schema loads). */
14
+ columnCount: number;
15
+ /** Rows of a full page: the page size. */
16
+ rowCount: number;
17
+ density: ResourceTableDensity;
18
+ /** Draw the toolbar's outline too (until the schema says what the toolbar holds). */
19
+ toolbar: boolean;
20
+ /** "cards" below the container breakpoint, else "table". */
21
+ layout: "table" | "cards";
22
+ /** A checkbox column leads (the table has bulk actions). */
23
+ selectable: boolean;
24
+ /** An actions column ends each row (edit, delete, `rowActions`). */
25
+ actions: boolean;
26
+ }
27
+ /**
28
+ * The default Loading slot: the real layout in outline (toolbar, header row
29
+ * with the real column count, a full page of rows at the current density,
30
+ * the pagination row), so nothing moves when the data arrives. Hidden from
31
+ * screen readers: the table's live region says "Loading…" instead.
32
+ */
33
+ export declare function TableSkeleton(props: ResourceTableLoadingProps): React.ReactElement;
34
+ /** What the NoRecords slot receives: the resource has no rows at all yet. */
35
+ export interface ResourceTableNoRecordsProps {
36
+ /** The resource as a sentence names it, plural ("events"). */
37
+ resourceLabel: string;
38
+ /** One of them ("event"). */
39
+ itemLabel: string;
40
+ /** Set when the table knows how to create one and the role may: offer it. */
41
+ onCreate?: () => void;
42
+ createLabel: string;
43
+ }
44
+ /** "No events yet": explains, and offers the create action when there is one. */
45
+ export declare function NoRecords(props: ResourceTableNoRecordsProps): React.ReactElement;
46
+ /** One active filter, in words ("Status: Draft"). */
47
+ export interface ResourceTableActiveFilter {
48
+ field: string;
49
+ label: string;
50
+ value: string;
51
+ }
52
+ /** What the NoResults slot receives: rows exist, but none match the search or filters. */
53
+ export interface ResourceTableNoResultsProps {
54
+ resourceLabel: string;
55
+ /** The search term, or "" when the search box is empty. */
56
+ search: string;
57
+ /** The filters narrowing the list, in words. */
58
+ filters: readonly ResourceTableActiveFilter[];
59
+ /** Resets the search and every filter (pinned `params` stay). */
60
+ onClearFilters: () => void;
61
+ }
62
+ /** "No events match": says what is narrowing the list and offers to clear it. */
63
+ export declare function NoResults(props: ResourceTableNoResultsProps): React.ReactElement;
64
+ /** The default Error slot: what failed, in plain words, with "Try again" when it can help. */
65
+ export declare function TableError(props: ErrorSlotProps): React.ReactElement;
66
+ /**
67
+ * The default RefetchError slot: a failed refresh over rows already shown.
68
+ * The rows stay, and this one line says the list may be out of date.
69
+ */
70
+ export declare function RefetchError(props: ErrorSlotProps): React.ReactElement;
71
+ /** What the Forbidden slot receives: the list itself answered 403. */
72
+ export interface ResourceTableForbiddenProps {
73
+ resourceLabel: string;
74
+ /** The server's refusal (its own explanation, when it gave one, is in `data.detail`). */
75
+ error: unknown;
76
+ }
77
+ /** "You can't view events": no retry, since asking again changes nothing. */
78
+ export declare function Forbidden(props: ResourceTableForbiddenProps): React.ReactElement;
79
+ /** What the Offline slot receives while the browser has no connection. */
80
+ export interface ResourceTableOfflineProps {
81
+ resourceLabel: string;
82
+ /** Rows loaded before the connection dropped are still on screen. */
83
+ hasRows: boolean;
84
+ onRetry: () => void;
85
+ }
86
+ /**
87
+ * The default Offline slot. With rows on screen it is one line above them.
88
+ * Without rows it is the offline error panel. The table reloads by itself
89
+ * once the connection is back.
90
+ */
91
+ export declare function OfflineNotice(props: ResourceTableOfflineProps): React.ReactElement;
@@ -0,0 +1,25 @@
1
+ import { RequestHandler } from 'msw';
2
+ import { ResourceTableStateStore } from './url-state';
3
+ export declare const WIDE_COLUMN_COUNT = 50;
4
+ /** Field names of the wide resource: `name`, then 49 metrics. */
5
+ export declare const WIDE_FIELDS: readonly string[];
6
+ export declare const wideOpenApi: Record<string, unknown>;
7
+ export declare function wideHandlers(total?: number): RequestHandler[];
8
+ export declare const LARGE_EVENT_COUNT = 100000;
9
+ /** `/api/events/` with `total` rows. Only the requested page is ever built. */
10
+ export declare function largeHandlers(total?: number): RequestHandler[];
11
+ /**
12
+ * The events schema as a backend with `Access(write=Owned("organizer") |
13
+ * HasRole("staff"), create=HasRole("staff"))` and deletes for admins only
14
+ * publishes it.
15
+ */
16
+ export declare const accessOpenApi: Record<string, unknown>;
17
+ /**
18
+ * `accessOpenApi` as a role that may not read `capacity` (`Karni.field_roles`):
19
+ * the per-role schema leaves the field out, so the column is absent.
20
+ */
21
+ export declare const partialOpenApi: Record<string, unknown>;
22
+ /** A ResourceTableStateStore over a plain object: what a router adapter looks like. */
23
+ export declare function memoryStateStore(initial?: Record<string, string>): ResourceTableStateStore & {
24
+ values: Record<string, string>;
25
+ };
@@ -0,0 +1,25 @@
1
+ import { ColumnDef } from '@tanstack/react-table';
2
+ import * as React from "react";
3
+ export declare const SELECT_COLUMN = "__select__";
4
+ export declare const ACTIONS_COLUMN = "__actions__";
5
+ export declare function isStructural(columnId: string): boolean;
6
+ export declare function selectionColumn<T>(options: {
7
+ rowLabel: (row: T) => string;
8
+ resourceLabel: string;
9
+ }): ColumnDef<T>;
10
+ export interface RowActionsOptions<T> {
11
+ rowActions?: (row: T) => React.ReactNode;
12
+ onEdit?: (row: T) => void;
13
+ /** Whether this row's edit action is offered (the role may change it). */
14
+ canEdit: (row: T) => boolean;
15
+ /** Present when deletes are enabled. Asks to confirm, then deletes. */
16
+ onDelete?: (row: T, trigger: HTMLButtonElement) => void;
17
+ canDelete: (row: T) => boolean;
18
+ /** Button geometry for the current density. */
19
+ buttonClassName: string;
20
+ }
21
+ /** The actions of one row, in the order a row reads: edit, custom, delete. */
22
+ export declare function RowActions<T>(props: RowActionsOptions<T> & {
23
+ row: T;
24
+ }): React.ReactElement;
25
+ export declare function actionsColumn<T>(options: RowActionsOptions<T>): ColumnDef<T>;
@@ -0,0 +1,16 @@
1
+ /**
2
+ * ResourceTable's own icons (pagination arrows, create, view options). Inline
3
+ * SVG with `currentColor` strokes, like the primitives' icons.
4
+ * Internal to @karni/ui.
5
+ */
6
+ import * as React from "react";
7
+ type IconProps = React.SVGAttributes<SVGSVGElement>;
8
+ export declare function ChevronLeftIcon(props: IconProps): React.ReactElement;
9
+ export declare function ChevronRightIcon(props: IconProps): React.ReactElement;
10
+ export declare function FirstPageIcon(props: IconProps): React.ReactElement;
11
+ export declare function LastPageIcon(props: IconProps): React.ReactElement;
12
+ export declare function PlusIcon(props: IconProps): React.ReactElement;
13
+ export declare function SlidersIcon(props: IconProps): React.ReactElement;
14
+ export declare function FilterOffIcon(props: IconProps): React.ReactElement;
15
+ export declare function InboxIcon(props: IconProps): React.ReactElement;
16
+ export {};
@@ -1,16 +1,40 @@
1
+ import { DefaultError, DefaultOptions } from '@tanstack/react-query';
1
2
  import { SortingState } from '@tanstack/react-table';
2
3
  /**
3
4
  * 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`.
5
+ * Column ids equal schema field names verbatim (that IS the ordering
6
+ * contract, D5), so `[{id: "title", desc: true}]` becomes `-title`.
6
7
  */
7
8
  export declare function sortingToOrdering(sorting: SortingState): string | undefined;
9
+ /** `"-title,starts_at"` → TanStack sorting state (the inverse of sortingToOrdering). */
10
+ export declare function orderingToSorting(ordering: string): SortingState;
11
+ /** The HTTP status of a KarniApiError-shaped error, if it has one. */
12
+ export declare function statusOf(error: unknown): number | undefined;
8
13
  /**
9
14
  * Duck-typed 404 check (works with KarniApiError or any error exposing a
10
15
  * numeric `status`), used for the D5 page-clamp: a list 404 with page > 1
11
16
  * decrements the page and refetches as loading, never as an error.
12
17
  */
13
18
  export declare function isNotFoundError(error: unknown): boolean;
19
+ /** The `retry` default of the app's QueryClient (`undefined`: TanStack's own). */
20
+ export type RetryDefault = NonNullable<DefaultOptions["queries"]>["retry"];
21
+ /**
22
+ * The list query's retry rule, under the app's own. A request the server
23
+ * refused (4xx) fails the same way again, so it is never retried, except a
24
+ * timeout (408) or a rate limit (429). Anything else (5xx, the network)
25
+ * retries as the app's QueryClient `retry` default says: three tries when it
26
+ * sets none, and none at all with `retry: false`, which shows the error at once.
27
+ */
28
+ export declare function listRetry(appDefault: RetryDefault): (failureCount: number, error: DefaultError) => boolean;
29
+ /** "loose-slots" → "loose slots": the resource as a sentence names it. */
30
+ export declare function resourceWords(resource: string): string;
31
+ /**
32
+ * A conservative English singular ("events" → "event", "categories" →
33
+ * "category", "addresses" → "address"). ResourceTable's `itemLabel` prop
34
+ * replaces it where it guesses wrong.
35
+ */
36
+ export declare function singular(plural: string): string;
37
+ export declare function capitalised(text: string): string;
14
38
  export declare function pageCountFor(count: number | undefined, pageSize: number): number;
15
39
  /** Safe record view of an unknown row (rows are plain JSON objects). */
16
40
  export declare function asRecord(value: unknown): Record<string, unknown>;
@@ -18,7 +42,7 @@ export declare function asRecord(value: unknown): Record<string, unknown>;
18
42
  export declare function errorMessage(error: unknown): string;
19
43
  /**
20
44
  * Display label for an enum member: ALL_CAPS and lower_snake constants are
21
- * prettified ("DRAFT" → "Draft", "in_progress" → "In progress"); mixed-case
45
+ * prettified ("DRAFT" → "Draft", "in_progress" → "In progress"). Mixed-case
22
46
  * values ("iOS") pass through untouched.
23
47
  */
24
48
  export declare function enumLabel(value: string | number, labels?: Readonly<Record<string, string>>): string;