@declarion/react 0.1.76 → 0.1.78

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.
@@ -1,4 +1,4 @@
1
- import type { Action, ActionCondition, Schema } from "../types/schema";
1
+ import type { Action, ActionCondition, LocalizedString, Schema } from "../types/schema";
2
2
  /**
3
3
  * Evaluates action conditions against row data.
4
4
  * Returns true if the action is available (all conditions pass).
@@ -34,6 +34,8 @@ export declare function actionHasParams(action: Action): boolean;
34
34
  export declare function fullActionCode(entityCode: string | undefined, actionCode: string): string;
35
35
  /**
36
36
  * Checks if an action is an association navigation action.
37
+ * Reason: per new wire format (unified-toolbar plan §1-§2), dispatch mode is
38
+ * encoded by which dispatch field is set; the `association` field is the mode.
37
39
  */
38
40
  export declare function isAssociationAction(action: Action): boolean;
39
41
  /**
@@ -44,3 +46,98 @@ export declare function findListScreenForEntity(schema: Schema | undefined, enti
44
46
  route: string;
45
47
  icon: string;
46
48
  } | undefined;
49
+ /**
50
+ * Resolved leaf node. Kind discriminator "action".
51
+ * Enabled defaults true from server; client sets false when enabled_if fails.
52
+ * Reason carries the localized tooltip text when Enabled is false.
53
+ *
54
+ * Wire format (plan §8): {"kind":"action","code":"approve","enabled":true,"reason":null}
55
+ */
56
+ export interface ResolvedActionLeaf {
57
+ kind: "action";
58
+ code: string;
59
+ enabled: boolean;
60
+ reason: string | null;
61
+ }
62
+ /**
63
+ * Resolved group node. Kind discriminator "group".
64
+ * Items is always non-empty after pruning (empty groups are dropped).
65
+ *
66
+ * Wire format (plan §8): {"kind":"group","name":{...},"icon":"...","items":[...]}
67
+ */
68
+ export interface ResolvedActionGroup {
69
+ kind: "group";
70
+ name?: LocalizedString;
71
+ icon?: string;
72
+ items: ResolvedActionLayoutNode[];
73
+ }
74
+ /**
75
+ * Discriminated union for a resolved action layout node.
76
+ * Narrow via node.kind === "action" or node.kind === "group".
77
+ */
78
+ export type ResolvedActionLayoutNode = ResolvedActionLeaf | ResolvedActionGroup;
79
+ /**
80
+ * applyRowState walks an RBAC-pruned ResolvedActionLayoutNode[] and applies
81
+ * per-row visible_if / enabled_if evaluation (plan §4, §5).
82
+ *
83
+ * Drop conditions:
84
+ * - visible_if conditions not satisfied by row -> leaf dropped
85
+ * - Empty groups after all children are dropped -> group dropped
86
+ *
87
+ * Disable conditions:
88
+ * - enabled_if conditions not satisfied by row -> leaf enabled=false + reason set
89
+ * - For bulk (rowSelection.length > 1): disabled if ANY selected row fails enabled_if
90
+ * (plan §5: "disabled if ANY row fails"). Reason is from the first failing row.
91
+ *
92
+ * @param layout RBAC-pruned tree from the server (or resolveActionLayout client-side).
93
+ * @param entityActions Entity-level actions map (entity wins on code collision).
94
+ * @param globalActions Global schema actions (fallback when entity doesn't declare).
95
+ * @param row The current row data for single-row context (detail / list_row).
96
+ * @param rowSelection All selected rows for bulk context (list_toolbar bulk mode).
97
+ * Pass empty array for non-bulk contexts.
98
+ */
99
+ export declare function applyRowState(layout: ResolvedActionLayoutNode[], entityActions: Record<string, Action> | undefined, globalActions: Record<string, Action> | undefined, row: Record<string, unknown>, rowSelection: Record<string, unknown>[]): ResolvedActionLayoutNode[];
100
+ /**
101
+ * Composes a flat ResolvedActionLayoutNode[] from an entity's actions map,
102
+ * filtered to those that include the given slot in their visibility array,
103
+ * sorted by position ascending (ties broken by declaration order).
104
+ *
105
+ * This is the fallback used when neither screen.action_layout nor
106
+ * entity.action_layout is present (plan §3: "runtime MUST compose a flat
107
+ * layout from all actions matching the slot's visibility[], sorted by
108
+ * position ascending").
109
+ *
110
+ * All leaves are emitted with enabled=true; client applies visible_if /
111
+ * enabled_if via applyRowState.
112
+ *
113
+ * @param actions Entity-level actions map from the (already RBAC-pruned) schema.
114
+ * @param slot Placement slot code (e.g. "detail", "list_row", "list_toolbar").
115
+ */
116
+ /**
117
+ * Filters a server-emitted ResolvedActionLayoutNode tree to a single slot,
118
+ * preserving group structure. Drops leaves whose action's visibility[] does
119
+ * not include the slot. Drops groups whose entire item list pruned out
120
+ * (plan §5 empty-group auto-hide). Additional filters via `opts`:
121
+ *
122
+ * - opts.bulkRequired: when set, gates list_toolbar leaves whose effective
123
+ * "bulk-required" semantic matches the request. The old wire-format flag
124
+ * `bulk_selection_required` was removed in unified-toolbar Phase 1 (the
125
+ * server now derives this from `handler.invoke`); until Task 38 replaces
126
+ * this code path the caller passes a predicate that decides per action.
127
+ * Reason: keep the slot+bulk filtering surface intact while removing the
128
+ * field; the call sites supply the legacy predicate.
129
+ * - opts.excludeIf: drop leaves where the predicate returns true. Common
130
+ * use: hide "create" / "edit" / "delete" style actions that the screen
131
+ * renders inline elsewhere. Reason: the old `excludeTypes` keyed on
132
+ * `action.type`, which is removed; callers now express the same intent
133
+ * as a field-presence predicate.
134
+ *
135
+ * Leaves whose code cannot be looked up are dropped (server should have
136
+ * pruned them; defensive).
137
+ */
138
+ export declare function filterLayoutForSlot(layout: ResolvedActionLayoutNode[], entityActions: Record<string, Action> | undefined, globalActions: Record<string, Action> | undefined, slot: string, opts?: {
139
+ bulkRequired?: boolean;
140
+ bulkRequiredPredicate?: (action: Action) => boolean;
141
+ excludeIf?: (action: Action) => boolean;
142
+ }): ResolvedActionLayoutNode[];
143
+ export declare function defaultFlatForSlot(actions: Record<string, Action> | undefined, slot: string): ResolvedActionLayoutNode[];
@@ -1,3 +1,4 @@
1
- import type { Schema } from "../types/schema";
1
+ import type { Entity, Schema } from "../types/schema";
2
2
  import type { PendingChildRow } from "../components/detail-layout/LayoutRenderer";
3
+ export declare function findMissingRequiredFields(entity: Entity, formData: Record<string, unknown>, isNew: boolean): string[];
3
4
  export declare function validatePendingChildren(pendingChildren: Record<string, PendingChildRow[]> | undefined, schema: Schema | undefined): Record<string, Record<string, string[]>>;
@@ -1,2 +1,9 @@
1
1
  import { type ClassValue } from "clsx";
2
2
  export declare function cn(...inputs: ClassValue[]): string;
3
+ export declare function isModifiedClick(e: {
4
+ metaKey: boolean;
5
+ ctrlKey: boolean;
6
+ shiftKey: boolean;
7
+ altKey: boolean;
8
+ button: number;
9
+ }): boolean;
@@ -1,2 +1,2 @@
1
- export type FieldType = "uuid" | "string" | "text" | "rich_text" | "int" | "float" | "bool" | "date" | "time" | "timestamp" | "email" | "url" | "phone" | "enum" | "json" | "ref" | "tags" | "multilang" | "multilang_text" | "structure" | "secret" | "password" | "decimal" | "string_array" | "int_array";
1
+ export type FieldType = "uuid" | "string" | "text" | "rich_text" | "int" | "float" | "bool" | "date" | "time" | "timestamp" | "email" | "url" | "phone" | "enum" | "json" | "ref" | "tags" | "multilang" | "multilang_text" | "structure" | "secret" | "password" | "decimal" | "string_array" | "int_array" | "bytes";
2
2
  export declare const FIELD_TYPES: readonly FieldType[];
@@ -27,6 +27,11 @@ export declare function getDisplayValue(row: Record<string, unknown> | null | un
27
27
  display_field?: string;
28
28
  };
29
29
  } | null | undefined): string | undefined;
30
+ export declare function resolveRefRecordLabel(record: Record<string, unknown>, displayField: string | undefined, fallback: string): string;
31
+ export declare function screenTitleForPath(pathname: string, screens: Record<string, Screen> | undefined): {
32
+ title: string;
33
+ icon?: string;
34
+ } | undefined;
30
35
  export declare function fieldDisplayName(field: EntityField, fieldName: string, entities?: Record<string, Entity>): string;
31
36
  export interface RefDisplay {
32
37
  field: string;
@@ -262,6 +267,13 @@ export interface Entity {
262
267
  /** Whether the entity participates in RBAC permission gating
263
268
  * (`entity:<code>:<op>` grants). Mirrors engine.Entity.Permissions. */
264
269
  permissions?: boolean;
270
+ /**
271
+ * Optional structural tree for placing and grouping actions across all
272
+ * placement slots. Absent = runtime composes a flat list from all actions
273
+ * matching each slot's visibility[], sorted by position ascending.
274
+ * Server delivers RBAC-pruned nodes in tagged-union wire format (plan §8).
275
+ */
276
+ action_layout?: ActionLayoutNode[];
265
277
  }
266
278
  export interface ExportConfig {
267
279
  enabled?: boolean;
@@ -377,10 +389,11 @@ export interface Screen {
377
389
  detail_screen?: string;
378
390
  fixed_filters?: Record<string, unknown>;
379
391
  views?: ViewDefinition[];
380
- /** Action slot promotion (rule-of-2). UI honors first 2. */
392
+ /** Action slot promotion for list screens (rule-of-2). UI honors first 2.
393
+ * Note: primary_record_actions (detail header) was removed in Task 5 of plan
394
+ * 2026-04-21-action-toolbar-priority-overflow. Use action_layout instead. */
381
395
  primary_actions?: string[];
382
396
  primary_bulk_actions?: string[];
383
- primary_record_actions?: string[];
384
397
  /** "open" (default) navigates to detail; "peek" opens QuickPeek drawer. */
385
398
  on_row_click?: "open" | "peek";
386
399
  quick_peek?: QuickPeekConfig;
@@ -391,6 +404,13 @@ export interface Screen {
391
404
  * Either gate being false hides the toolbar Export menu and makes the
392
405
  * POST endpoint return 403 EXPORT_DISABLED. */
393
406
  export?: ExportConfig;
407
+ /**
408
+ * Optional per-screen structural tree for placing and grouping actions.
409
+ * When set, fully replaces entity.action_layout for this screen.
410
+ * Absent = inherit entity.action_layout (then default-flat if also absent).
411
+ * Server delivers RBAC-pruned nodes in tagged-union wire format (plan §8).
412
+ */
413
+ action_layout?: ActionLayoutNode[];
394
414
  }
395
415
  export interface QuickPeekConfig {
396
416
  enabled?: boolean;
@@ -508,28 +528,147 @@ export interface UIStep {
508
528
  focus_field?: string;
509
529
  show_toast?: UIShowToastStep;
510
530
  }
531
+ /**
532
+ * Leaf node in an action_layout tree. References a single action by code.
533
+ * Wire format (plan §8): {"kind": "action", "code": "<code>"}
534
+ */
535
+ export interface ActionLayoutLeaf {
536
+ kind: "action";
537
+ code: string;
538
+ }
539
+ /**
540
+ * Branch node in an action_layout tree. Groups child nodes under a labelled dropdown.
541
+ * Wire format (plan §8): {"kind": "group", "name": {...}, "icon": "...", "items": [...]}
542
+ * Items is always non-empty (enforced by loader).
543
+ */
544
+ export interface ActionLayoutGroup {
545
+ kind: "group";
546
+ name?: LocalizedString;
547
+ icon?: string;
548
+ items: ActionLayoutNode[];
549
+ }
550
+ /**
551
+ * Discriminated union of all action_layout tree nodes.
552
+ * Narrow via `node.kind === "action"` (leaf) or `node.kind === "group"` (branch).
553
+ */
554
+ export type ActionLayoutNode = ActionLayoutLeaf | ActionLayoutGroup;
555
+ /**
556
+ * Single row-state condition for visible_if / enabled_if / conditions.
557
+ * Operators: eq | ne | in | not_in | gt | lt | ge | le.
558
+ */
559
+ export interface ActionConditionEntry {
560
+ field?: string;
561
+ op?: string;
562
+ value?: unknown;
563
+ status?: string[];
564
+ }
565
+ /**
566
+ * Array of row-state conditions with AND semantics. YAML accepts both single
567
+ * object and array form (see Go ActionConditionList unmarshal); wire format
568
+ * from server is always array (server normalizes during schema response).
569
+ */
570
+ export type ActionConditionList = ActionConditionEntry[];
511
571
  export interface Action {
572
+ /**
573
+ * Server-side handler code. One of four dispatch fields - see plan §1-§2.
574
+ * Reason: the new wire format encodes dispatch mode by which field is set
575
+ * (handler | association | widget | href), so an explicit `type` enum is
576
+ * redundant and was removed. Exactly one of the four should be set per action.
577
+ */
512
578
  handler?: string;
513
- type?: string;
579
+ /**
580
+ * Association code for nav-only actions that navigate to a related list.
581
+ * Reason: same dispatch-by-field-presence rule; see plan §1-§2.
582
+ */
514
583
  association?: string;
584
+ /**
585
+ * SDK widget registry name. Phase 2 wires the registry; today the field
586
+ * is plumbed through the schema and consumed by Task 38's toolbar pipeline.
587
+ * Reason: lets actions render bespoke UI (e.g. inline picker) instead of
588
+ * server-handled forms, without inventing a new entity per widget.
589
+ */
590
+ widget?: string;
591
+ /**
592
+ * Client-side navigation URL template (supports `${id}` and other row tokens).
593
+ * Reason: replaces the implicit "edit"/"create" type enum - any href-driven
594
+ * action (open a custom screen, deep-link a report) uses this field; the
595
+ * server never dispatches it.
596
+ */
597
+ href?: string;
598
+ /**
599
+ * Widget-specific opaque metadata. Schema/Action loader passes through
600
+ * untouched; the widget consumes its own keys.
601
+ * Reason: keeps the Action shape stable as new widgets ship without
602
+ * growing the typed surface for each one.
603
+ */
604
+ config?: Record<string, unknown>;
515
605
  entity?: string;
516
606
  display: {
517
607
  name: LocalizedString;
518
608
  icon: string;
609
+ /**
610
+ * Promotes the action to a header button (vs. dropdown item).
611
+ * Reason: explicit per-action UX control replaces the old position<=10
612
+ * heuristic, lets schema authors mark their headline action even when
613
+ * other actions sort earlier.
614
+ */
615
+ primary?: boolean;
616
+ /**
617
+ * Render as icon-only button (no label text).
618
+ * Reason: lets a dense toolbar host high-frequency actions without
619
+ * cluttering the bar with labels; tooltip still shows display.name.
620
+ */
621
+ icon_only?: boolean;
622
+ /**
623
+ * Button size hint. Default "md".
624
+ * Reason: secondary toolbars (associated blocks, inline editors) need
625
+ * smaller buttons than primary header bars - the schema author picks.
626
+ */
627
+ size?: "sm" | "md" | "lg";
628
+ /**
629
+ * Keyboard shortcut hint (e.g. "Cmd+S"). Rendered in tooltip/menu.
630
+ * Reason: discoverability for power users; binding is wired in Task 38.
631
+ */
632
+ shortcut?: string;
633
+ /**
634
+ * Localized help text for tooltip/popover.
635
+ * Reason: separate from display.name so the title stays terse while a
636
+ * longer explanation surfaces on hover.
637
+ */
638
+ help?: LocalizedString;
519
639
  };
520
640
  visibility?: string[];
521
641
  confirmation?: LocalizedString;
522
642
  conditions?: ActionCondition[];
523
- /** Promote to header/bulk-bar button (max 2 per slot honored by UI). */
524
- primary?: boolean;
643
+ /**
644
+ * Ordering within a placement slot and overflow priority. Lower = earlier
645
+ * in bar AND last to overflow. Default 0; ties broken by declaration order.
646
+ * Step-10 convention. See plan §1 and §3.
647
+ */
648
+ position?: number;
649
+ /**
650
+ * Hide the action when condition does not hold against current row.
651
+ * Distinct from RBAC hide and from `enabled_if` disable. AND semantics
652
+ * across array elements. See plan §1, §5.
653
+ */
654
+ visible_if?: ActionConditionList;
655
+ /**
656
+ * Disable the action (greyed out + tooltip) when condition does not hold.
657
+ * For bulk-mode multi-row selection, disabled if ANY selected row fails.
658
+ * See plan §1, §5.
659
+ */
660
+ enabled_if?: ActionConditionList;
661
+ /**
662
+ * Localized tooltip shown when action is disabled by `enabled_if`.
663
+ * Required when `enabled_if` is set (loader validates in Task 3).
664
+ */
665
+ enabled_if_reason?: LocalizedString;
525
666
  /** Forces confirm + danger styling. */
526
667
  destructive?: boolean;
527
668
  /** UI shows a toast instead of a blocking result modal; adds a navigation link if progress_screen is set. */
528
669
  long_running?: boolean;
529
670
  /** Screen code to navigate to after a long_running action starts (resolved via schema.screens). */
530
671
  progress_screen?: string;
531
- /** When true, action shown grayed until >=1 row selected. */
532
- bulk_selection_required?: boolean;
533
672
  resolved_params?: OrderedMap<HandlerParam>;
534
673
  /**
535
674
  * Default values of params marked hidden+default by the action. The UI
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@declarion/react",
3
- "version": "0.1.76",
3
+ "version": "0.1.78",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "description": "React SDK for Declarion, the schema-driven business apps platform.",
@@ -66,6 +66,7 @@
66
66
  "zustand": "^5.0.0"
67
67
  },
68
68
  "devDependencies": {
69
+ "@axe-core/react": "^4.11.3",
69
70
  "@tailwindcss/typography": "^0.5.19",
70
71
  "@tailwindcss/vite": "^4.2.4",
71
72
  "@testing-library/dom": "^10.4.1",
@@ -75,6 +76,7 @@
75
76
  "@types/react": "^19.0.0",
76
77
  "@types/react-dom": "^19.0.0",
77
78
  "@vitejs/plugin-react": "^6.0.1",
79
+ "axe-core": "^4.11.4",
78
80
  "eslint": "^10.2.1",
79
81
  "eslint-plugin-react-hooks": "^7.1.1",
80
82
  "jsdom": "^29.0.1",
@@ -1,30 +0,0 @@
1
- import { type ReactNode } from "react";
2
- import { type Action, type Screen } from "../../types/schema";
3
- export type DetailMode = "view" | "edit" | "create";
4
- export interface DetailPageProps {
5
- mode: DetailMode;
6
- onModeChange: (m: DetailMode) => void;
7
- onClose: () => void;
8
- onSave: () => void | Promise<void>;
9
- onDelete?: () => void;
10
- title: string;
11
- recordId?: string;
12
- /** When true, opens in edit mode with the dirty badge — used by peek-promote. */
13
- initialDirty?: boolean;
14
- dirty: boolean;
15
- /** Optional sub-header row (pipeline / priority / source / score chips). */
16
- subHeader?: ReactNode;
17
- /** Avatar or leading badge rendered before the title. */
18
- leading?: ReactNode;
19
- /** Full YAML screen so primary_record_actions is honored. */
20
- screen?: Screen;
21
- /** Actions available on this record (resolved from entity + screen). */
22
- actions: Record<string, Action>;
23
- executeAction: (code: string, action: Action, params: Record<string, unknown>) => Promise<unknown>;
24
- /** Optional right-rail slot — maps to the design's ActivityPanel / sidebar. */
25
- sidebar?: ReactNode;
26
- /** Optional footer metadata strip ("Created … · Updated … · Version 7"). */
27
- footerMeta?: ReactNode;
28
- children?: ReactNode;
29
- }
30
- export declare function DetailPage({ mode, onModeChange, onClose, onSave, onDelete, title, recordId, initialDirty, dirty, subHeader, leading, screen, actions, executeAction, sidebar, footerMeta, children, }: DetailPageProps): import("react/jsx-runtime").JSX.Element;