create-middag-ui 0.24.0 → 0.26.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 CHANGED
@@ -43,7 +43,7 @@ Then start developing:
43
43
 
44
44
  ```bash
45
45
  cd ui
46
- npm run dev:mock
46
+ npm run dev
47
47
  ```
48
48
 
49
49
  Your mock opens at `http://localhost:5174` (Moodle), `5175` (WordPress), or `5176` (Custom).
package/cli.js CHANGED
@@ -257,8 +257,12 @@ if (hostKey === "moodle" && moodleComponent) {
257
257
  scaffoldMoodlePlugin(targetDir, moodleComponent);
258
258
  scaffoldMoodleTailwind(targetDir);
259
259
  scaffoldMoodleAdapters(targetDir, moodleComponent);
260
- scaffoldMoodleAdminShell(targetDir);
261
- success(`Moodle: AMD plugin + Tailwind + adapters + AdminShell for ${ moodleComponent }`);
260
+ // AdminShell uses Pro-only chrome (SidebarNav/PageHeader from @middag-io/react-pro);
261
+ // only scaffold it for PRO. FREE Moodle uses BasicShell via registerDefaults().
262
+ if (isPro) {
263
+ scaffoldMoodleAdminShell(targetDir);
264
+ }
265
+ success(`Moodle: AMD plugin + Tailwind + adapters${isPro ? " + AdminShell" : ""} for ${ moodleComponent }`);
262
266
  }
263
267
 
264
268
  // ── Step 9: npm install ──────────────────────────────────────────────────
@@ -296,11 +300,12 @@ if (installOk) {
296
300
 
297
301
  blank();
298
302
  console.log(" Your scaffold includes:");
299
- console.log(" src/pages/dashboard.ts \u2190 starter: metric_card + dense_table");
300
- console.log(" src/pages/connectors.ts \u2190 intermediate: card_grid + status_strip");
301
- console.log(" src/pages/settings.ts \u2190 advanced: tabbed_panel + form_panel");
303
+ console.log(" src/pages/tasks.ts \u2190 list: metric_card + dense_table + row actions");
304
+ console.log(" src/pages/task-form.ts \u2190 create/edit: schema-driven form_panel");
305
+ console.log(" src/pages/task-detail.ts \u2190 detail: field/value via dense_table");
306
+ console.log(" src/pages/login.ts \u2190 login: basic shell + form_panel");
302
307
  console.log(" src/blocks/hello-block.tsx \u2190 custom block example (rename me!)");
303
- console.log(" src/app.tsx \u2190 hash-based page router");
308
+ console.log(" src/app.tsx \u2190 page router (mirrors middag-php-demo-standalone)");
304
309
 
305
310
  blank();
306
311
  console.log(` Production build for ${host.name}:`);
package/lib/scaffold.js CHANGED
@@ -198,7 +198,7 @@ export function scaffoldTsconfig(targetDir) {
198
198
  noUnusedLocals: true,
199
199
  noUnusedParameters: true,
200
200
  skipLibCheck: true,
201
- paths: { "@/*": ["./src/*"] },
201
+ paths: { "@/*": ["./src/*"], "@mock/*": ["./mock/*"] },
202
202
  baseUrl: ".",
203
203
  },
204
204
  include: ["src", "mock"],
@@ -634,100 +634,132 @@ export type { PageContract, BlockDescriptor, SharedProps } from "@middag-io/reac
634
634
  // ── Page contract examples ──────────────────────────────────────────────
635
635
 
636
636
  /**
637
- * Scaffold 3 progressive page contract examples in src/pages/.
637
+ * Scaffold the Tasks CRUD page contracts in src/pages/.
638
+ *
639
+ * These mirror the PageContracts emitted by the middag-php-demo-standalone
640
+ * backend (a contract-driven Tasks CRUD): login, list, create/edit form and
641
+ * detail. The mock dev server renders them with the free engine; a real host
642
+ * sends the same contracts over Inertia.
638
643
  */
639
644
  export function scaffoldPageExamples(targetDir) {
640
645
  ensureDir(join(targetDir, "src", "pages"));
641
646
 
642
- // ── Starter: dashboard.ts (metric_card + dense_table) ───────────────
643
- const dashboardPath = join(targetDir, "src", "pages", "dashboard.ts");
644
- if (!skipIfExists(dashboardPath, "src/pages/dashboard.ts")) {
647
+ // ── login.ts (basic shell + form_panel) ─────────────────────────────
648
+ const loginPath = join(targetDir, "src", "pages", "login.ts");
649
+ if (!skipIfExists(loginPath, "src/pages/login.ts")) {
645
650
  writeFile(
646
- dashboardPath,
651
+ loginPath,
647
652
  `/**
648
- * Dashboard page contract \u2014 STARTER example.
649
- *
650
- * Demonstrates the "dashboard" layout with two block types:
651
- * - metric_card (KPI indicators in the metrics region)
652
- * - dense_table (data table in the content region)
653
+ * Login page contract.
653
654
  *
654
- * Layout regions used: metrics, content
655
+ * Uses the "basic" shell and a form_panel block. form_panel ships in the free
656
+ * @middag-io/react engine; its heavy deps (react-hook-form + zod) load on demand.
655
657
  */
656
658
  import type { PageContract } from "@middag-io/react";
657
659
 
658
- export const dashboardContract: PageContract = {
660
+ export const loginContract: PageContract = {
659
661
  version: "1",
660
- shell: "product",
661
- page: {
662
- key: "dashboard",
663
- title: "Dashboard",
664
- breadcrumbs: [{ label: "Home", href: "#/" }],
665
- },
662
+ shell: "basic",
663
+ page: { key: "demo.login", title: "Sign in", subtitle: "Use any credentials, this is a mock." },
666
664
  layout: {
667
- template: "dashboard",
665
+ template: "stack",
668
666
  regions: {
669
- metrics: [
670
- {
671
- key: "total_users",
672
- type: "metric_card",
673
- data: {
674
- label: "Total Users",
675
- value: "1,284",
676
- delta: "+12%",
677
- deltaDirection: "positive",
678
- icon: "users",
679
- },
680
- },
681
- {
682
- key: "active_sessions",
683
- type: "metric_card",
684
- data: {
685
- label: "Active Sessions",
686
- value: "342",
687
- delta: "+5%",
688
- deltaDirection: "positive",
689
- icon: "activity",
690
- },
691
- },
667
+ content: [
692
668
  {
693
- key: "completion_rate",
694
- type: "metric_card",
669
+ key: "login",
670
+ type: "form_panel",
695
671
  data: {
696
- label: "Completion Rate",
697
- value: "87%",
698
- delta: "-2%",
699
- deltaDirection: "negative",
700
- icon: "chart-line",
672
+ action: "/login",
673
+ method: "post",
674
+ schema: [
675
+ { kind: "field", key: "email", component: "email", props: { label: "Email", required: true, placeholder: "you@example.com" } },
676
+ { kind: "field", key: "password", component: "password", props: { label: "Password", required: true } },
677
+ ],
678
+ values: {},
679
+ errors: {},
680
+ meta: { submitLabel: "Sign in" },
701
681
  },
702
682
  },
703
683
  ],
684
+ },
685
+ },
686
+ };
687
+ `,
688
+ "src/pages/login.ts",
689
+ );
690
+ }
691
+
692
+ // ── tasks.ts (metric_card + dense_table with row actions) ────────────
693
+ const tasksPath = join(targetDir, "src", "pages", "tasks.ts");
694
+ if (!skipIfExists(tasksPath, "src/pages/tasks.ts")) {
695
+ writeFile(
696
+ tasksPath,
697
+ `/**
698
+ * Tasks list page contract, the dashboard after login.
699
+ *
700
+ * Demonstrates two free blocks plus row-level actions:
701
+ * - metric_card (task count)
702
+ * - dense_table (rows, rowHref for navigation, rowActions for edit/delete)
703
+ */
704
+ import type { PageContract } from "@middag-io/react";
705
+
706
+ export const tasksContract: PageContract = {
707
+ version: "1",
708
+ shell: "product",
709
+ page: {
710
+ key: "demo.tasks",
711
+ title: "Tasks",
712
+ subtitle: "A contract-driven CRUD, mirroring the middag-php-demo-standalone backend.",
713
+ actions: [
714
+ { id: "new", label: "New task", intent: "primary", icon: "plus", target: { kind: "link", href: "/tasks/new" } },
715
+ ],
716
+ },
717
+ layout: {
718
+ template: "stack",
719
+ regions: {
704
720
  content: [
705
721
  {
706
- key: "welcome",
707
- type: "hello_block",
708
- data: { greeting: "Hello from a custom block!", name: "Developer", role: "MIDDAG UI" },
722
+ key: "task_count",
723
+ type: "metric_card",
724
+ data: { label: "Tasks", value: 3, icon: "list-check" },
709
725
  },
710
726
  {
711
- key: "recent_activity",
727
+ key: "tasks",
712
728
  type: "dense_table",
713
- title: "Recent Activity",
729
+ title: "All tasks",
714
730
  data: {
715
731
  columns: [
716
- { key: "user", label: "User", sortable: true },
717
- { key: "action", label: "Action" },
718
- { key: "date", label: "Date", sortable: true },
732
+ { key: "title", label: "Title" },
719
733
  { key: "status", label: "Status", variant: "badge" },
734
+ { key: "priority", label: "Priority" },
735
+ { key: "created", label: "Created", variant: "timestamp" },
720
736
  ],
721
737
  rows: [
722
- { id: 1, user: "Alice Johnson", action: "Completed module", date: "2024-01-15", status: "Completed" },
723
- { id: 2, user: "Bob Smith", action: "Started course", date: "2024-01-15", status: "Pending" },
724
- { id: 3, user: "Carol Davis", action: "Failed quiz", date: "2024-01-14", status: "Failed" },
725
- { id: 4, user: "Dave Wilson", action: "Enrolled", date: "2024-01-14", status: "Active" },
726
- { id: 5, user: "Eve Brown", action: "Completed course", date: "2024-01-13", status: "Completed" },
738
+ { id: 1, title: "Write onboarding docs", status: "open", priority: "high", created: "2024-05-30" },
739
+ { id: 2, title: "Review pull request", status: "open", priority: "normal", created: "2024-05-29" },
740
+ { id: 3, title: "Ship the release", status: "done", priority: "low", created: "2024-05-28" },
727
741
  ],
728
- pagination: { page: 1, perPage: 10, total: 5, lastPage: 1 },
729
- sort: { column: "date", direction: "desc" },
742
+ pagination: { page: 1, perPage: 10, total: 3, lastPage: 1 },
743
+ sort: { column: "created", direction: "desc" },
730
744
  filters: { available: [], applied: {} },
745
+ rowHref: "/tasks/{id}",
746
+ rowActions: [
747
+ { id: "edit", label: "Edit", intent: "secondary", icon: "pencil", target: { kind: "link", href: "/tasks/{id}/edit" } },
748
+ {
749
+ id: "delete",
750
+ label: "Delete",
751
+ intent: "danger",
752
+ icon: "trash",
753
+ target: { kind: "request", endpoint: "/tasks/{id}", method: "delete" },
754
+ confirmation: {
755
+ title: "Delete task",
756
+ message: "Remove this task? This cannot be undone.",
757
+ variant: "danger",
758
+ confirmLabel: "Delete",
759
+ cancelLabel: "Cancel",
760
+ },
761
+ },
762
+ ],
731
763
  },
732
764
  },
733
765
  ],
@@ -735,99 +767,133 @@ export const dashboardContract: PageContract = {
735
767
  },
736
768
  };
737
769
  `,
738
- "src/pages/dashboard.ts",
770
+ "src/pages/tasks.ts",
739
771
  );
740
772
  }
741
773
 
742
- // ── Intermediate: connectors.ts (card_grid + status_strip + detail_panel)
743
- const connectorsPath = join(targetDir, "src", "pages", "connectors.ts");
744
- if (!skipIfExists(connectorsPath, "src/pages/connectors.ts")) {
774
+ // ── task-form.ts (form_panel, create + edit share one schema) ────────
775
+ const taskFormPath = join(targetDir, "src", "pages", "task-form.ts");
776
+ if (!skipIfExists(taskFormPath, "src/pages/task-form.ts")) {
745
777
  writeFile(
746
- connectorsPath,
778
+ taskFormPath,
747
779
  `/**
748
- * Connectors page contract \u2014 INTERMEDIATE example.
780
+ * Task create/edit page contracts, schema-driven form_panel.
749
781
  *
750
- * Demonstrates the "sidebar" layout with three block types:
751
- * - card_grid (connector cards in the main region)
752
- * - status_strip (health indicators in the aside)
753
- * - detail_panel (metadata in the aside)
754
- *
755
- * Layout regions used: main, aside
782
+ * One field schema drives both create (empty values, POST) and edit (prefilled
783
+ * values, PUT). Shows text, textarea, select, radio, date, int, switch,
784
+ * entity_picker and a conditional field (done_reason appears when status = done).
756
785
  */
757
- import type { PageContract } from "@middag-io/react";
786
+ import type { PageContract, FormSchemaNode } from "@middag-io/react";
758
787
 
759
- export const connectorsContract: PageContract = {
788
+ const taskFormSchema: FormSchemaNode[] = [
789
+ { kind: "field", key: "title", component: "text", props: { label: "Title", required: true, placeholder: "What needs doing?" } },
790
+ { kind: "field", key: "notes", component: "textarea", props: { label: "Notes", rows: 4 } },
791
+ {
792
+ kind: "field",
793
+ key: "priority",
794
+ component: "select",
795
+ props: {
796
+ label: "Priority",
797
+ required: true,
798
+ options: [
799
+ { value: "low", label: "Low" },
800
+ { value: "normal", label: "Normal" },
801
+ { value: "high", label: "High" },
802
+ ],
803
+ },
804
+ },
805
+ {
806
+ kind: "field",
807
+ key: "status",
808
+ component: "radio",
809
+ props: {
810
+ label: "Status",
811
+ required: true,
812
+ options: [
813
+ { value: "open", label: "Open" },
814
+ { value: "done", label: "Done" },
815
+ ],
816
+ },
817
+ },
818
+ {
819
+ kind: "field",
820
+ key: "done_reason",
821
+ component: "text",
822
+ props: {
823
+ label: "Done reason",
824
+ helpText: "Shown only when status is Done.",
825
+ visible_when: { field: "status", operator: "equals", value: "done" },
826
+ required_when: { field: "status", operator: "equals", value: "done" },
827
+ },
828
+ },
829
+ { kind: "field", key: "due_on", component: "date", props: { label: "Due date" } },
830
+ { kind: "field", key: "estimate_minutes", component: "int", props: { label: "Estimate (minutes)", min: 0, max: 100000 } },
831
+ {
832
+ kind: "field",
833
+ key: "parent_task",
834
+ component: "entity_picker",
835
+ props: {
836
+ label: "Parent task",
837
+ options: [
838
+ { value: "1", label: "Write onboarding docs" },
839
+ { value: "2", label: "Review pull request" },
840
+ ],
841
+ },
842
+ },
843
+ { kind: "field", key: "notify", component: "switch", props: { label: "Notify on change" } },
844
+ ];
845
+
846
+ export const newTaskContract: PageContract = {
760
847
  version: "1",
761
848
  shell: "product",
762
849
  page: {
763
- key: "connectors",
764
- title: "Connectors",
765
- breadcrumbs: [
766
- { label: "Home", href: "#/" },
767
- { label: "Connectors" },
768
- ],
769
- actions: [
770
- { id: "add", label: "Add Connector", intent: "primary", icon: "plus" },
771
- ],
850
+ key: "demo.tasks.create",
851
+ title: "New task",
852
+ breadcrumbs: [{ label: "Tasks", href: "/" }, { label: "New task" }],
772
853
  },
773
854
  layout: {
774
- template: "sidebar",
855
+ template: "stack",
775
856
  regions: {
776
- main: [
857
+ content: [
777
858
  {
778
- key: "connector_grid",
779
- type: "card_grid",
780
- title: "Available Connectors",
859
+ key: "task_form",
860
+ type: "form_panel",
781
861
  data: {
782
- variant: "connector",
783
- columns: [
784
- { key: "name", label: "Name" },
785
- { key: "type", label: "Type" },
786
- { key: "status", label: "Status", kind: "status" },
787
- ],
788
- rows: [
789
- { id: 1, name: "Moodle LMS", type: "LMS", status: "Active", icon: "graduation-cap", href: "#/connectors" },
790
- { id: 2, name: "Google Workspace", type: "SSO", status: "Active", icon: "shield", href: "#/connectors" },
791
- { id: 3, name: "Stripe", type: "Payment", status: "Inactive", icon: "credit-card", href: "#/connectors" },
792
- { id: 4, name: "Mailchimp", type: "Email", status: "Active", icon: "mail", href: "#/connectors" },
793
- ],
862
+ action: "/tasks",
863
+ method: "post",
864
+ schema: taskFormSchema,
865
+ values: { priority: "normal", status: "open", notify: true },
866
+ errors: {},
867
+ meta: { submitLabel: "Create task", cancelHref: "/" },
794
868
  },
795
869
  },
796
870
  ],
797
- aside: [
798
- {
799
- key: "connector_health",
800
- type: "status_strip",
801
- title: "Health Overview",
802
- data: {
803
- score: 75,
804
- tone: "success",
805
- items: [
806
- { key: "uptime", label: "Uptime", value: "99.9%", appearance: "success" },
807
- { key: "sync", label: "Last Sync", value: "2 min ago", appearance: "success" },
808
- { key: "errors", label: "Errors (24h)", value: "3", appearance: "warning" },
809
- { key: "queue", label: "Queue", value: "0", appearance: "success" },
810
- ],
811
- },
812
- },
871
+ },
872
+ },
873
+ };
874
+
875
+ export const editTaskContract: PageContract = {
876
+ version: "1",
877
+ shell: "product",
878
+ page: {
879
+ key: "demo.tasks.edit",
880
+ title: "Edit task",
881
+ breadcrumbs: [{ label: "Tasks", href: "/" }, { label: "Edit task" }],
882
+ },
883
+ layout: {
884
+ template: "stack",
885
+ regions: {
886
+ content: [
813
887
  {
814
- key: "connector_detail",
815
- type: "detail_panel",
816
- title: "Selected Connector",
888
+ key: "task_form",
889
+ type: "form_panel",
817
890
  data: {
818
- sections: [
819
- {
820
- id: "overview",
821
- title: "Overview",
822
- fields: [
823
- { key: "provider", label: "Provider", value: "Moodle LMS" },
824
- { key: "version", label: "API Version", value: "4.3.2" },
825
- { key: "connected", label: "Connected Since", value: "2024-01-01", kind: "timestamp" },
826
- { key: "status", label: "Status", value: "Active", kind: "status" },
827
- { key: "endpoint", label: "Endpoint", value: "https://moodle.example.com/webservice/rest", kind: "code", copyable: true },
828
- ],
829
- },
830
- ],
891
+ action: "/tasks/1",
892
+ method: "put",
893
+ schema: taskFormSchema,
894
+ values: { title: "Write onboarding docs", priority: "high", status: "open", notify: true },
895
+ errors: {},
896
+ meta: { submitLabel: "Save changes", cancelHref: "/" },
831
897
  },
832
898
  },
833
899
  ],
@@ -835,36 +901,32 @@ export const connectorsContract: PageContract = {
835
901
  },
836
902
  };
837
903
  `,
838
- "src/pages/connectors.ts",
904
+ "src/pages/task-form.ts",
839
905
  );
840
906
  }
841
907
 
842
- // ── Advanced: settings.ts (tabbed_panel + form_panel + link_list) ────
843
- const settingsPath = join(targetDir, "src", "pages", "settings.ts");
844
- if (!skipIfExists(settingsPath, "src/pages/settings.ts")) {
908
+ // ── task-detail.ts (dense_table as a field/value detail view) ────────
909
+ const taskDetailPath = join(targetDir, "src", "pages", "task-detail.ts");
910
+ if (!skipIfExists(taskDetailPath, "src/pages/task-detail.ts")) {
845
911
  writeFile(
846
- settingsPath,
912
+ taskDetailPath,
847
913
  `/**
848
- * Settings page contract \u2014 ADVANCED example.
849
- *
850
- * Demonstrates the "stack" layout with nested block types:
851
- * - tabbed_panel (tabs that contain other blocks)
852
- * - form_panel (schema-driven form inside a tab)
853
- * - link_list (navigation links inside a tab)
914
+ * Task detail page contract, a read-only field/value view.
854
915
  *
855
- * Layout regions used: content
916
+ * Reuses dense_table as a two-column key/value display (the demo backend does
917
+ * the same), plus an Edit action in the page header.
856
918
  */
857
919
  import type { PageContract } from "@middag-io/react";
858
920
 
859
- export const settingsContract: PageContract = {
921
+ export const taskDetailContract: PageContract = {
860
922
  version: "1",
861
923
  shell: "product",
862
924
  page: {
863
- key: "settings",
864
- title: "Settings",
865
- breadcrumbs: [
866
- { label: "Home", href: "#/" },
867
- { label: "Settings" },
925
+ key: "demo.tasks.show",
926
+ title: "Task detail",
927
+ breadcrumbs: [{ label: "Tasks", href: "/" }, { label: "Task detail" }],
928
+ actions: [
929
+ { id: "edit", label: "Edit", intent: "secondary", icon: "pencil", target: { kind: "link", href: "/tasks/1/edit" } },
868
930
  ],
869
931
  },
870
932
  layout: {
@@ -872,115 +934,23 @@ export const settingsContract: PageContract = {
872
934
  regions: {
873
935
  content: [
874
936
  {
875
- key: "settings_tabs",
876
- type: "tabbed_panel",
937
+ key: "task",
938
+ type: "dense_table",
939
+ title: "Write onboarding docs",
877
940
  data: {
878
- defaultTab: "general",
879
- tabs: [
880
- {
881
- key: "general",
882
- label: "General",
883
- icon: "settings",
884
- blocks: [
885
- {
886
- key: "general_form",
887
- type: "form_panel",
888
- data: {
889
- action: "/api/settings/general",
890
- method: "put",
891
- schema: [
892
- {
893
- kind: "section",
894
- id: "site",
895
- label: "Site Settings",
896
- children: [
897
- {
898
- kind: "field",
899
- key: "site_name",
900
- component: "text",
901
- props: { label: "Site Name", placeholder: "My Platform", required: true },
902
- },
903
- {
904
- kind: "field",
905
- key: "site_url",
906
- component: "url",
907
- props: { label: "Site URL", placeholder: "https://example.com" },
908
- },
909
- {
910
- kind: "field",
911
- key: "timezone",
912
- component: "select",
913
- props: {
914
- label: "Timezone",
915
- options: [
916
- { value: "UTC", label: "UTC" },
917
- { value: "America/Sao_Paulo", label: "S\\u00e3o Paulo (BRT)" },
918
- { value: "America/New_York", label: "New York (EST)" },
919
- { value: "Europe/London", label: "London (GMT)" },
920
- ],
921
- },
922
- },
923
- ],
924
- },
925
- {
926
- kind: "section",
927
- id: "features",
928
- label: "Features",
929
- children: [
930
- {
931
- kind: "field",
932
- key: "enable_notifications",
933
- component: "switch",
934
- props: { label: "Enable Notifications", helpText: "Send email notifications for important events" },
935
- },
936
- {
937
- kind: "field",
938
- key: "enable_analytics",
939
- component: "switch",
940
- props: { label: "Enable Analytics", helpText: "Track user activity and generate reports" },
941
- },
942
- {
943
- kind: "field",
944
- key: "maintenance_mode",
945
- component: "switch",
946
- props: { label: "Maintenance Mode", helpText: "Show maintenance page to non-admin users" },
947
- },
948
- ],
949
- },
950
- ],
951
- values: {
952
- site_name: "My Platform",
953
- site_url: "https://example.com",
954
- timezone: "UTC",
955
- enable_notifications: true,
956
- enable_analytics: true,
957
- maintenance_mode: false,
958
- },
959
- errors: {},
960
- meta: { submitLabel: "Save Changes", cancelHref: "#/" },
961
- },
962
- },
963
- ],
964
- },
965
- {
966
- key: "notifications",
967
- label: "Notifications",
968
- icon: "bell",
969
- blocks: [
970
- {
971
- key: "notification_links",
972
- type: "link_list",
973
- data: {
974
- items: [
975
- { label: "Email Templates", href: "#/settings", icon: "mail", description: "Customize email notification templates" },
976
- { label: "Webhook Endpoints", href: "#/settings", icon: "webhook", description: "Configure webhook delivery endpoints" },
977
- { label: "Notification Rules", href: "#/settings", icon: "filter", description: "Set up conditional notification routing" },
978
- ],
979
- },
980
- },
981
- ],
982
- },
941
+ columns: [
942
+ { key: "field", label: "Field" },
943
+ { key: "value", label: "Value" },
944
+ ],
945
+ rows: [
946
+ { id: "title", field: "Title", value: "Write onboarding docs" },
947
+ { id: "status", field: "Status", value: "open" },
948
+ { id: "priority", field: "Priority", value: "high" },
949
+ { id: "due_on", field: "Due date", value: "2024-06-10" },
983
950
  ],
951
+ pagination: { page: 1, perPage: 10, total: 4, lastPage: 1 },
952
+ sort: { column: null, direction: null },
953
+ filters: { available: [], applied: {} },
984
954
  },
985
955
  },
986
956
  ],
@@ -988,7 +958,7 @@ export const settingsContract: PageContract = {
988
958
  },
989
959
  };
990
960
  `,
991
- "src/pages/settings.ts",
961
+ "src/pages/task-detail.ts",
992
962
  );
993
963
  }
994
964
  }
@@ -1009,15 +979,33 @@ let _navigate: ((to: string) => void) | null = null;
1009
979
 
1010
980
  export function setMockNavigate(fn: (to: string) => void) { _navigate = fn; }
1011
981
 
982
+ type MockOpts = {
983
+ only?: string[];
984
+ preserveState?: boolean;
985
+ preserveScroll?: boolean;
986
+ onSuccess?: () => void;
987
+ onFinish?: () => void;
988
+ onError?: (errors: Record<string, string>) => void;
989
+ };
990
+
991
+ // A synchronous mock resolves instantly — fire the lifecycle callbacks the
992
+ // engine expects (e.g. useLazyTabs marks a tab loaded in onSuccess; blocks
993
+ // reset loading flags in onFinish), else those UI states stick forever.
994
+ const settle = (o?: MockOpts) => { o?.onSuccess?.(); o?.onFinish?.(); };
995
+
1012
996
  export const router = {
1013
- get: (url: string) => { _navigate ? _navigate(url) : console.log("[mock] GET", url); },
1014
- post: (url: string) => { console.log("[mock] POST", url); },
1015
- put: (url: string) => { console.log("[mock] PUT", url); },
1016
- patch: (url: string) => { console.log("[mock] PATCH", url); },
1017
- delete: (url: string) => { console.log("[mock] DELETE", url); },
1018
- reload: () => { window.location.reload(); },
1019
- visit: (url: string) => { _navigate ? _navigate(url) : console.log("[mock] VISIT", url); },
1020
- on: () => () => {},
997
+ get: (url: string, _params?: Record<string, unknown>, opts?: MockOpts) => {
998
+ if (_navigate) _navigate(url); else console.log("[mock] GET", url);
999
+ settle(opts);
1000
+ },
1001
+ post: (url: string, _data?: unknown, opts?: MockOpts) => { console.log("[mock] POST", url); settle(opts); },
1002
+ put: (url: string, _data?: unknown, opts?: MockOpts) => { console.log("[mock] PUT", url); settle(opts); },
1003
+ patch: (url: string, _data?: unknown, opts?: MockOpts) => { console.log("[mock] PATCH", url); settle(opts); },
1004
+ delete: (url: string, opts?: MockOpts) => { console.log("[mock] DELETE", url); settle(opts); },
1005
+ reload: (opts?: MockOpts) => { settle(opts); },
1006
+ visit: (url: string) => { if (_navigate) _navigate(url); else console.log("[mock] VISIT", url); },
1007
+ on: (_event?: string, _cb?: (event: unknown) => void) => () => {},
1008
+ poll: (_intervalMs?: number, _reload?: unknown, _opts?: unknown) => ({ stop: () => {}, start: () => {}, destroy: () => {} }),
1021
1009
  };
1022
1010
  `, "mock/adapters/inertia-core.ts");
1023
1011
  }
@@ -1067,6 +1055,35 @@ export const Link = React.forwardRef<HTMLAnchorElement, MockLinkProps>(function
1067
1055
  return React.createElement("a", { ...rest, href: href ?? "#", ref, onClick: handleClick }, children);
1068
1056
  });
1069
1057
 
1058
+ // ── Inertia v3 primitives the engine imports ──────────────────────────
1059
+ // @middag-io/react imports Deferred/WhenVisible (lazy blocks) and useRemember
1060
+ // (DataTable view-prefs) from @inertiajs/react. In the no-server mock the data
1061
+ // is already present, so render children immediately; useRemember = useState.
1062
+
1063
+ export function Deferred({ children }: {
1064
+ data: string | string[];
1065
+ fallback?: React.ReactNode;
1066
+ rescue?: React.ReactNode;
1067
+ children: React.ReactNode;
1068
+ }) {
1069
+ return children;
1070
+ }
1071
+
1072
+ export function WhenVisible({ children }: {
1073
+ data?: string | string[];
1074
+ buffer?: number;
1075
+ as?: string;
1076
+ always?: boolean;
1077
+ fallback?: React.ReactNode;
1078
+ children: React.ReactNode;
1079
+ }) {
1080
+ return children;
1081
+ }
1082
+
1083
+ export function useRemember<T>(initialState: T, _key?: string): [T, React.Dispatch<React.SetStateAction<T>>] {
1084
+ return React.useState(initialState);
1085
+ }
1086
+
1070
1087
  export { router };
1071
1088
  `, "mock/adapters/inertia-react.ts");
1072
1089
  }
@@ -1182,9 +1199,10 @@ import { ContractPage, I18nProvider } from "@middag-io/react";
1182
1199
  import type { PageContract } from "@middag-io/react";
1183
1200
  import { PageProvider } from "@mock/adapters/inertia-react";
1184
1201
  import { setMockNavigate } from "@mock/adapters/inertia-core";
1185
- import { dashboardContract } from "./pages/dashboard";
1186
- import { connectorsContract } from "./pages/connectors";
1187
- import { settingsContract } from "./pages/settings";
1202
+ import { loginContract } from "./pages/login";
1203
+ import { tasksContract } from "./pages/tasks";
1204
+ import { newTaskContract, editTaskContract } from "./pages/task-form";
1205
+ import { taskDetailContract } from "./pages/task-detail";
1188
1206
 
1189
1207
  function NavigateBridge() {
1190
1208
  const navigate = useNavigate();
@@ -1204,12 +1222,10 @@ const sharedProps = {
1204
1222
  function buildNavigation(activeKey: string) {
1205
1223
  return {
1206
1224
  tree: [
1207
- { key: "overview.dashboard", label: "Dashboard", icon: "home", href: "/", children: [] },
1208
- { key: "integration.connectors", label: "Connectors", icon: "plug", href: "/connectors", children: [] },
1209
- ],
1210
- footer: [
1211
- { key: "system.settings", label: "Settings", icon: "settings", href: "/settings", children: [] },
1225
+ { key: "tasks", label: "Tasks", icon: "list-check", href: "/", children: [] },
1226
+ { key: "tasks.new", label: "New task", icon: "plus", href: "/tasks/new", children: [] },
1212
1227
  ],
1228
+ footer: [],
1213
1229
  activeKey,
1214
1230
  };
1215
1231
  }
@@ -1229,9 +1245,11 @@ export function App() {
1229
1245
  <BrowserRouter>
1230
1246
  <NavigateBridge />
1231
1247
  <Routes>
1232
- <Route path="/" element={<MockRoute contract={dashboardContract} activeKey="overview.dashboard" />} />
1233
- <Route path="/connectors" element={<MockRoute contract={connectorsContract} activeKey="integration.connectors" />} />
1234
- <Route path="/settings" element={<MockRoute contract={settingsContract} activeKey="system.settings" />} />
1248
+ <Route path="/login" element={<MockRoute contract={loginContract} activeKey="" />} />
1249
+ <Route path="/" element={<MockRoute contract={tasksContract} activeKey="tasks" />} />
1250
+ <Route path="/tasks/new" element={<MockRoute contract={newTaskContract} activeKey="tasks.new" />} />
1251
+ <Route path="/tasks/:id/edit" element={<MockRoute contract={editTaskContract} activeKey="tasks" />} />
1252
+ <Route path="/tasks/:id" element={<MockRoute contract={taskDetailContract} activeKey="tasks" />} />
1235
1253
  </Routes>
1236
1254
  </BrowserRouter>
1237
1255
  </I18nProvider>
@@ -1759,9 +1777,10 @@ createRoot(document.getElementById("root")!).render(
1759
1777
  `import { useState, useEffect } from "react";
1760
1778
  import { ContractPage, I18nProvider } from "@middag-io/react";
1761
1779
  import type { PageContract } from "@middag-io/react";
1762
- import { dashboardContract } from "./pages/dashboard";
1763
- import { connectorsContract } from "./pages/connectors";
1764
- import { settingsContract } from "./pages/settings";
1780
+ import { loginContract } from "./pages/login";
1781
+ import { tasksContract } from "./pages/tasks";
1782
+ import { newTaskContract, editTaskContract } from "./pages/task-form";
1783
+ import { taskDetailContract } from "./pages/task-detail";
1765
1784
 
1766
1785
  /**
1767
1786
  * Route map \u2014 hash fragment to PageContract.
@@ -1771,9 +1790,11 @@ import { settingsContract } from "./pages/settings";
1771
1790
  * you build them.
1772
1791
  */
1773
1792
  const routes: Record<string, PageContract> = {
1774
- "/": dashboardContract,
1775
- "/connectors": connectorsContract,
1776
- "/settings": settingsContract,
1793
+ "/": tasksContract,
1794
+ "/login": loginContract,
1795
+ "/tasks/new": newTaskContract,
1796
+ "/tasks/edit": editTaskContract,
1797
+ "/tasks/show": taskDetailContract,
1777
1798
  };
1778
1799
 
1779
1800
  function getRoute(): string {
@@ -1789,7 +1810,7 @@ export function App() {
1789
1810
  return () => window.removeEventListener("hashchange", onHashChange);
1790
1811
  }, []);
1791
1812
 
1792
- const contract = routes[route] || dashboardContract;
1813
+ const contract = routes[route] || tasksContract;
1793
1814
 
1794
1815
  // Expose contract for usePage() mock adapter
1795
1816
  (window as any).__MIDDAG_MOCK_CONTRACT__ = contract;
@@ -1824,41 +1845,23 @@ import { router } from "./inertia-core";
1824
1845
  function getActiveKey(): string {
1825
1846
  const hash = window.location.hash.replace("#", "") || "/";
1826
1847
  const map: Record<string, string> = {
1827
- "/": "overview.dashboard",
1828
- "/connectors": "integration.connectors",
1829
- "/settings": "system.settings",
1848
+ "/": "tasks",
1849
+ "/tasks/new": "tasks.new",
1830
1850
  };
1831
- return map[hash] || "overview.dashboard";
1851
+ return map[hash] || "tasks";
1832
1852
  }
1833
1853
 
1834
1854
  function buildNavigation() {
1835
1855
  const activeKey = getActiveKey();
1836
1856
  const sections = [
1837
1857
  {
1838
- key: "overview",
1839
- label: "Overview",
1840
- icon: "house",
1858
+ key: "tasks",
1859
+ label: "Tasks",
1860
+ icon: "list-check",
1841
1861
  group: "main" as const,
1842
1862
  items: [
1843
- { key: "overview.dashboard", label: "Dashboard", href: "#/", active: activeKey === "overview.dashboard", children: [] },
1844
- ],
1845
- },
1846
- {
1847
- key: "integration",
1848
- label: "Integration",
1849
- icon: "plug",
1850
- group: "main" as const,
1851
- items: [
1852
- { key: "integration.connectors", label: "Connectors", href: "#/connectors", active: activeKey === "integration.connectors", children: [] },
1853
- ],
1854
- },
1855
- {
1856
- key: "system",
1857
- label: "System",
1858
- icon: "settings",
1859
- group: "system" as const,
1860
- items: [
1861
- { key: "system.settings", label: "Settings", href: "#/settings", active: activeKey === "system.settings", children: [] },
1863
+ { key: "tasks", label: "Tasks", href: "#/", active: activeKey === "tasks", children: [] },
1864
+ { key: "tasks.new", label: "New task", href: "#/tasks/new", active: activeKey === "tasks.new", children: [] },
1862
1865
  ],
1863
1866
  },
1864
1867
  ];
@@ -1916,6 +1919,34 @@ export const Link = React.forwardRef<HTMLAnchorElement, MockLinkProps>(function
1916
1919
  return React.createElement("a", { ...rest, href: href ?? "#", ref, onClick: handleClick }, children);
1917
1920
  });
1918
1921
 
1922
+ // ── Inertia v3 primitives the engine imports ──────────────────────────
1923
+ // @middag-io/react imports Deferred/WhenVisible + useRemember from
1924
+ // @inertiajs/react; mock renders children eagerly, useRemember = useState.
1925
+
1926
+ export function Deferred({ children }: {
1927
+ data: string | string[];
1928
+ fallback?: React.ReactNode;
1929
+ rescue?: React.ReactNode;
1930
+ children: React.ReactNode;
1931
+ }) {
1932
+ return children;
1933
+ }
1934
+
1935
+ export function WhenVisible({ children }: {
1936
+ data?: string | string[];
1937
+ buffer?: number;
1938
+ as?: string;
1939
+ always?: boolean;
1940
+ fallback?: React.ReactNode;
1941
+ children: React.ReactNode;
1942
+ }) {
1943
+ return children;
1944
+ }
1945
+
1946
+ export function useRemember<T>(initialState: T, _key?: string): [T, React.Dispatch<React.SetStateAction<T>>] {
1947
+ return React.useState(initialState);
1948
+ }
1949
+
1919
1950
  export { router };
1920
1951
  `,
1921
1952
  "mock/adapters/inertia-react.ts",
@@ -1933,15 +1964,27 @@ export { router };
1933
1964
  * Vite alias redirects @inertiajs/core imports here.
1934
1965
  * In production, the real Inertia package handles this.
1935
1966
  */
1967
+ type MockOpts = {
1968
+ only?: string[];
1969
+ preserveState?: boolean;
1970
+ preserveScroll?: boolean;
1971
+ onSuccess?: () => void;
1972
+ onFinish?: () => void;
1973
+ onError?: (errors: Record<string, string>) => void;
1974
+ };
1975
+
1976
+ const settle = (o?: MockOpts) => { o?.onSuccess?.(); o?.onFinish?.(); };
1977
+
1936
1978
  export const router = {
1937
- get: (url: string) => { window.location.hash = url; },
1938
- post: (url: string) => { console.log("[mock] POST", url); },
1939
- put: (url: string) => { console.log("[mock] PUT", url); },
1940
- patch: (url: string) => { console.log("[mock] PATCH", url); },
1941
- delete: (url: string) => { console.log("[mock] DELETE", url); },
1942
- reload: () => { window.location.reload(); },
1979
+ get: (url: string, _params?: Record<string, unknown>, opts?: MockOpts) => { window.location.hash = url; settle(opts); },
1980
+ post: (url: string, _data?: unknown, opts?: MockOpts) => { console.log("[mock] POST", url); settle(opts); },
1981
+ put: (url: string, _data?: unknown, opts?: MockOpts) => { console.log("[mock] PUT", url); settle(opts); },
1982
+ patch: (url: string, _data?: unknown, opts?: MockOpts) => { console.log("[mock] PATCH", url); settle(opts); },
1983
+ delete: (url: string, opts?: MockOpts) => { console.log("[mock] DELETE", url); settle(opts); },
1984
+ reload: (opts?: MockOpts) => { settle(opts); },
1943
1985
  visit: (url: string) => { window.location.hash = url; },
1944
- on: () => () => {},
1986
+ on: (_event?: string, _cb?: (event: unknown) => void) => () => {},
1987
+ poll: (_intervalMs?: number, _reload?: unknown, _opts?: unknown) => ({ stop: () => {}, start: () => {}, destroy: () => {} }),
1945
1988
  };
1946
1989
  `,
1947
1990
  "mock/adapters/inertia-core.ts",
@@ -171,7 +171,7 @@ export function scaffoldMockEntities(targetDir) {
171
171
  /**
172
172
  * Move page contract examples to `mock/page-contracts/` for PRO path.
173
173
  *
174
- * In PRO, the 3 demo contracts (dashboard, connectors, settings) live
174
+ * In PRO, the demo contracts (login, tasks, task-form, task-detail) live
175
175
  * in mock/page-contracts/ instead of src/pages/ because they are dev-only
176
176
  * mock data, not production React components.
177
177
  *
@@ -186,7 +186,7 @@ export function scaffoldMockPageContracts(targetDir) {
186
186
  const destDir = join(targetDir, "mock", "page-contracts");
187
187
  ensureDir(destDir);
188
188
 
189
- const files = ["dashboard.ts", "connectors.ts", "settings.ts"];
189
+ const files = ["login.ts", "tasks.ts", "task-form.ts", "task-detail.ts"];
190
190
 
191
191
  for (const file of files) {
192
192
  const src = join(srcDir, file);
@@ -9,29 +9,21 @@ export function buildNavigation(activeKey: string) {
9
9
  return {
10
10
  tree: [
11
11
  {
12
- key: "overview.dashboard",
13
- label: "Dashboard",
14
- icon: "home",
12
+ key: "tasks",
13
+ label: "Tasks",
14
+ icon: "list-check",
15
15
  href: "/",
16
16
  children: [],
17
17
  },
18
18
  {
19
- key: "integration.connectors",
20
- label: "Connectors",
21
- icon: "plug",
22
- href: "/connectors",
23
- children: [],
24
- },
25
- ],
26
- footer: [
27
- {
28
- key: "system.settings",
29
- label: "Settings",
30
- icon: "settings",
31
- href: "/settings",
19
+ key: "tasks.new",
20
+ label: "New task",
21
+ icon: "plus",
22
+ href: "/tasks/new",
32
23
  children: [],
33
24
  },
34
25
  ],
26
+ footer: [],
35
27
  activeKey,
36
28
  };
37
29
  }
@@ -11,9 +11,10 @@ import type { PageContract } from "@middag-io/react";
11
11
  import { mockEntities } from "./entities";
12
12
  import { buildNavigation } from "./navigation";
13
13
  import { sharedProps, mockDemoPageProps } from "./data";
14
- import { dashboardContract } from "./page-contracts/dashboard";
15
- import { connectorsContract } from "./page-contracts/connectors";
16
- import { settingsContract } from "./page-contracts/settings";
14
+ import { loginContract } from "./page-contracts/login";
15
+ import { tasksContract } from "./page-contracts/tasks";
16
+ import { newTaskContract, editTaskContract } from "./page-contracts/task-form";
17
+ import { taskDetailContract } from "./page-contracts/task-detail";
17
18
  import DemoPage from "../src/pages/DemoPage";
18
19
 
19
20
  // Resolve shell at module level (not inside render — avoids react-hooks/static-components)
@@ -63,24 +64,26 @@ export function AppRoutes() {
63
64
  return (
64
65
  <>
65
66
  {/* Contract pages (rendered by lib ContractPage) */}
67
+ <Route path="/login" element={<MockRoute contract={loginContract} activeKey="" />} />
68
+ <Route path="/" element={<MockRoute contract={tasksContract} activeKey="tasks" />} />
66
69
  <Route
67
- path="/"
68
- element={<MockRoute contract={dashboardContract} activeKey="overview.dashboard" />}
70
+ path="/tasks/new"
71
+ element={<MockRoute contract={newTaskContract} activeKey="tasks.new" />}
69
72
  />
70
73
  <Route
71
- path="/connectors"
72
- element={<MockRoute contract={connectorsContract} activeKey="integration.connectors" />}
74
+ path="/tasks/:id/edit"
75
+ element={<MockRoute contract={editTaskContract} activeKey="tasks" />}
73
76
  />
74
77
  <Route
75
- path="/settings"
76
- element={<MockRoute contract={settingsContract} activeKey="system.settings" />}
78
+ path="/tasks/:id"
79
+ element={<MockRoute contract={taskDetailContract} activeKey="tasks" />}
77
80
  />
78
81
 
79
82
  {/* Direct page (custom React component) */}
80
83
  <Route
81
84
  path="/demo"
82
85
  element={
83
- <MockDirectRoute activeKey="overview.demo" pageProps={mockDemoPageProps}>
86
+ <MockDirectRoute activeKey="tasks" pageProps={mockDemoPageProps}>
84
87
  <DemoPage />
85
88
  </MockDirectRoute>
86
89
  }
@@ -15,6 +15,8 @@ import { usePage } from "@inertiajs/react";
15
15
  interface DemoPageProps {
16
16
  title: string;
17
17
  items: Array<{ id: number; name: string; status: string }>;
18
+ // Inertia v3 usePage<T> constrains T to PageProps (string index signature).
19
+ [key: string]: unknown;
18
20
  }
19
21
 
20
22
  export default function DemoPage() {
@@ -15,14 +15,16 @@ import {
15
15
  SidebarHeader,
16
16
  SidebarProvider,
17
17
  useSidebar,
18
- SidebarNav,
19
- PageHeader,
20
18
  NavErrorBoundary,
21
19
  type ShellProps,
22
20
  type SharedProps,
23
21
  type PageMeta,
24
22
  type AdminTabsProps,
25
23
  } from "@middag-io/react";
24
+ // SidebarNav/PageHeader are Pro chrome — exported from the react-pro runtime
25
+ // subpath (see react-demo MockProductShell). This shell is PRO-only (scaffolded
26
+ // only when isPro in cli.js); FREE Moodle uses BasicShell via registerDefaults().
27
+ import { SidebarNav, PageHeader } from "@middag-io/react-pro/runtime";
26
28
  import { Tabs, TabsList, TabsTrigger } from "@middag-io/react/reui/tabs";
27
29
 
28
30
  // ── Inner shell (must be inside SidebarProvider) ──────────────────────────────
@@ -1,11 +1,12 @@
1
- import type {ContractPageProps, PageContract} from "@middag-io/react";
1
+ import type {PageContract} from "@middag-io/react";
2
2
  import {ContractPage} from "@middag-io/react";
3
3
  import {usePage} from "@inertiajs/react";
4
+ import type {ComponentType} from "react";
4
5
 
5
6
  // Direct pages — eager loaded (custom React pages in pages/)
6
7
  const directPages = import.meta.glob("../pages/**/*.tsx", { eager: true }) as Record<
7
8
  string,
8
- Record<string, unknown>
9
+ { default: ComponentType }
9
10
  >;
10
11
 
11
12
  /**
@@ -14,15 +15,11 @@ const directPages = import.meta.glob("../pages/**/*.tsx", { eager: true }) as Re
14
15
  */
15
16
  // eslint-disable-next-line react-refresh/only-export-components
16
17
  function InertiaContractPage() {
17
- const { props } = usePage<{
18
- contract: PageContract;
19
- help?: ContractPageProps["help"];
20
- inspector?: ContractPageProps["inspector"];
21
- }>();
22
- return <ContractPage contract={props.contract} help={props.help} inspector={props.inspector} />;
18
+ const { props } = usePage<{ contract: PageContract }>();
19
+ return <ContractPage contract={props.contract} />;
23
20
  }
24
21
 
25
- const contractPageModule = { default: InertiaContractPage };
22
+ const contractPageModule: { default: ComponentType } = { default: InertiaContractPage };
26
23
 
27
24
  /**
28
25
  * Resolve an Inertia page name to a React component module.
@@ -38,7 +35,7 @@ const contractPageModule = { default: InertiaContractPage };
38
35
  *
39
36
  * Direct pages can also use ContractPage internally (hybrid pattern).
40
37
  */
41
- export function resolvePageComponent(name: string): Record<string, unknown> {
38
+ export function resolvePageComponent(name: string): { default: ComponentType } {
42
39
  // Direct page: matching .tsx file in pages/
43
40
  const path = `../pages/${name}.tsx`;
44
41
  const page = directPages[path];
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * register — registration for this plugin's UI.
3
3
  *
4
- * Registers the 12 standard blocks plus shells, layouts, cell renderers, form
5
- * fields and icons. The 7 Pro/interactive blocks (form_panel, chart_panel,
6
- * kanban_board, flow_editor, form_builder, condition_tree, sentence_builder)
7
- * ship in @middag-io/react-pro and register via its registerProDefaults().
4
+ * Registers the 13 standard blocks plus shells, layouts, cell renderers, form
5
+ * fields and icons. The 6 Pro/interactive blocks (chart_panel, kanban_board,
6
+ * flow_editor, form_builder, condition_tree, sentence_builder) ship in
7
+ * @middag-io/react-pro and register via its registerProDefaults().
8
8
  *
9
9
  * For lean IIFE bundles (WordPress/Moodle), trim the blocks you don't use.
10
10
  *
@@ -39,6 +39,7 @@ import {
39
39
  CardGridBlock,
40
40
  ActionGridBlock,
41
41
  LinkListBlock,
42
+ FormPanelBlock,
42
43
  } from "@middag-io/react";
43
44
 
44
45
  let registered = false;
@@ -71,6 +72,8 @@ export function registerDefaults(): void {
71
72
  registerBlock("card_grid", CardGridBlock);
72
73
  registerBlock("action_grid", ActionGridBlock);
73
74
  registerBlock("link_list", LinkListBlock);
75
+ // form_panel pulls react-hook-form + zod; drop it if this bundle has no forms.
76
+ registerBlock("form_panel", FormPanelBlock);
74
77
 
75
78
  // Cell renderers (status, timestamp, link, boolean, etc.)
76
79
  registerDefaultCells();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-middag-ui",
3
- "version": "0.24.0",
3
+ "version": "0.26.0",
4
4
  "type": "module",
5
5
  "description": "Bootstrap a MIDDAG React UI layer in your Moodle or WordPress plugin",
6
6
  "scripts": {