@antelopejs/dms 0.4.8 → 0.5.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 (70) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/dist/hooks/invite-extensions.js +0 -2
  3. package/dist/hooks/invite-extensions.js.map +1 -1
  4. package/dist/implementations/dms/page.js +6 -7
  5. package/dist/implementations/dms/page.js.map +1 -1
  6. package/dist/implementations/dms/realtime.d.ts +2 -2
  7. package/dist/implementations/dms/realtime.js +9 -3
  8. package/dist/implementations/dms/realtime.js.map +1 -1
  9. package/dist/implementations/dms-base/table-view.d.ts +5 -0
  10. package/dist/implementations/dms-base/table-view.js +7 -1
  11. package/dist/implementations/dms-base/table-view.js.map +1 -1
  12. package/dist/realtime/index.js +0 -2
  13. package/dist/realtime/index.js.map +1 -1
  14. package/dist/realtime/registry.d.ts +1 -0
  15. package/dist/realtime/registry.js +9 -0
  16. package/dist/realtime/registry.js.map +1 -1
  17. package/dist/realtime/table-view.d.ts +3 -0
  18. package/dist/realtime/table-view.js +25 -0
  19. package/dist/realtime/table-view.js.map +1 -0
  20. package/dist/test/antelope.test.js +6 -0
  21. package/dist/test/antelope.test.js.map +1 -1
  22. package/dist/test/helpers/reload-host.d.ts +9 -0
  23. package/dist/test/helpers/reload-host.js +21 -0
  24. package/dist/test/helpers/reload-host.js.map +1 -0
  25. package/dist/test/integration/invite-resolution.test.js +0 -2
  26. package/dist/test/integration/invite-resolution.test.js.map +1 -1
  27. package/dist/test/reload-host/index.d.ts +2 -0
  28. package/dist/test/reload-host/index.js +32 -0
  29. package/dist/test/reload-host/index.js.map +1 -0
  30. package/dist/test/reload-host/package.json +8 -0
  31. package/dist/test/unit/frontend-renderer-range.test.d.ts +1 -0
  32. package/dist/test/unit/frontend-renderer-range.test.js +45 -0
  33. package/dist/test/unit/frontend-renderer-range.test.js.map +1 -0
  34. package/dist/test/unit/interfaces/dms/invite-extension.test.js +22 -24
  35. package/dist/test/unit/interfaces/dms/invite-extension.test.js.map +1 -1
  36. package/dist/test/unit/interfaces/dms/module-reload-registrations.test.d.ts +1 -0
  37. package/dist/test/unit/interfaces/dms/module-reload-registrations.test.js +51 -0
  38. package/dist/test/unit/interfaces/dms/module-reload-registrations.test.js.map +1 -0
  39. package/dist/test/unit/interfaces/dms/page-topics.test.d.ts +1 -0
  40. package/dist/test/unit/interfaces/dms/page-topics.test.js +194 -0
  41. package/dist/test/unit/interfaces/dms/page-topics.test.js.map +1 -0
  42. package/dist/test/unit/interfaces/dms-base/layouts.test.js +16 -0
  43. package/dist/test/unit/interfaces/dms-base/layouts.test.js.map +1 -1
  44. package/dist/test/unit/interfaces/dms-base/table-view-evaluation-order.fixture.d.ts +5 -0
  45. package/dist/test/unit/interfaces/dms-base/table-view-evaluation-order.fixture.js +82 -0
  46. package/dist/test/unit/interfaces/dms-base/table-view-evaluation-order.fixture.js.map +1 -0
  47. package/dist/test/unit/interfaces/dms-base/table-view-evaluation-order.test.d.ts +1 -0
  48. package/dist/test/unit/interfaces/dms-base/table-view-evaluation-order.test.js +24 -0
  49. package/dist/test/unit/interfaces/dms-base/table-view-evaluation-order.test.js.map +1 -0
  50. package/dist/test/unit/interfaces/dms-base/table-view-get-guard.test.js +10 -4
  51. package/dist/test/unit/interfaces/dms-base/table-view-get-guard.test.js.map +1 -1
  52. package/dist/test/unit/interfaces/dms-base/table-view-realtime.test.d.ts +1 -0
  53. package/dist/test/unit/interfaces/dms-base/table-view-realtime.test.js +139 -0
  54. package/dist/test/unit/interfaces/dms-base/table-view-realtime.test.js.map +1 -0
  55. package/dist/test/unit/utils/owned-registry.test.d.ts +1 -0
  56. package/dist/test/unit/utils/owned-registry.test.js +61 -0
  57. package/dist/test/unit/utils/owned-registry.test.js.map +1 -0
  58. package/dist/validation/auth.schema.d.ts +2 -2
  59. package/frontend-vue/layers/dms-layout/app/composables/layout/usePageFillHeight.ts +37 -0
  60. package/frontend-vue/layers/dms-layout/app/custom-layouts/DefaultLayout.vue +9 -0
  61. package/frontend-vue/layers/dms-layout/app/pages/[...slug].vue +16 -1
  62. package/frontend-vue/package.json +4 -1
  63. package/frontend-vue/pnpm-lock.yaml +8 -8
  64. package/frontend-vue/tests/page-fill-height.test.ts +352 -0
  65. package/package.json +4 -4
  66. package/skills/dms-module/REFERENCE.md +2 -1
  67. package/skills/dms-module/SKILL.md +5 -0
  68. package/dist/realtime/tableview-bridge.d.ts +0 -1
  69. package/dist/realtime/tableview-bridge.js +0 -38
  70. package/dist/realtime/tableview-bridge.js.map +0 -1
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const modules_1 = require("@antelopejs/interface-core/modules");
4
+ const owned_registry_1 = require("@antelopejs/interface-dms/utils/owned-registry");
5
+ const chai_1 = require("chai");
6
+ function generation(module, index) {
7
+ return { module, owner: `${module}#${index}` };
8
+ }
9
+ function addAs(context, registry, value) {
10
+ (0, modules_1.RunWithModuleContext)(context, () => registry.add(value));
11
+ }
12
+ function destroy(context) {
13
+ (0, modules_1.RunWithModuleContext)(context, () => {
14
+ modules_1.Events.ModuleDestroyed.emit(context.module);
15
+ });
16
+ }
17
+ describe("[unit] utils/owned-registry", () => {
18
+ it("releases what a generation added when it is destroyed, and only that", () => {
19
+ const registry = new owned_registry_1.OwnedRegistry();
20
+ const reloaded = generation("owned-registry-reloaded", 1);
21
+ const other = generation("owned-registry-other", 1);
22
+ addAs(reloaded, registry, "reloaded");
23
+ addAs(other, registry, "other");
24
+ destroy(reloaded);
25
+ (0, chai_1.expect)(registry.values()).to.deep.equal(["other"]);
26
+ destroy(other);
27
+ (0, chai_1.expect)(registry.values()).to.deep.equal([]);
28
+ });
29
+ it("keeps what the next generation of the same module adds", () => {
30
+ const registry = new owned_registry_1.OwnedRegistry();
31
+ const first = generation("owned-registry-next", 1);
32
+ const next = generation("owned-registry-next", 2);
33
+ addAs(first, registry, "first");
34
+ destroy(first);
35
+ addAs(next, registry, "next");
36
+ (0, chai_1.expect)(registry.values()).to.deep.equal(["next"]);
37
+ destroy(next);
38
+ });
39
+ it("lists the live values in the order they were added", () => {
40
+ const registry = new owned_registry_1.OwnedRegistry();
41
+ const owner = generation("owned-registry-order", 1);
42
+ for (const value of ["first", "second", "third"]) {
43
+ addAs(owner, registry, value);
44
+ }
45
+ (0, chai_1.expect)(registry.values()).to.deep.equal(["first", "second", "third"]);
46
+ destroy(owner);
47
+ });
48
+ it("removes only the oldest registration a matcher accepts", () => {
49
+ const registry = new owned_registry_1.OwnedRegistry();
50
+ const owner = generation("owned-registry-remove", 1);
51
+ for (const value of ["twice", "once", "twice"]) {
52
+ addAs(owner, registry, value);
53
+ }
54
+ registry.remove((value) => value === "twice");
55
+ (0, chai_1.expect)(registry.values()).to.deep.equal(["once", "twice"]);
56
+ (0, chai_1.expect)(registry.has((value) => value === "once")).to.equal(true);
57
+ (0, chai_1.expect)(registry.has((value) => value === "never")).to.equal(false);
58
+ destroy(owner);
59
+ });
60
+ });
61
+ //# sourceMappingURL=owned-registry.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"owned-registry.test.js","sourceRoot":"","sources":["../../../../src/test/unit/utils/owned-registry.test.ts"],"names":[],"mappings":"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"}
@@ -42,14 +42,14 @@ export declare const authSchema: {
42
42
  code: string;
43
43
  state: string;
44
44
  state_cookie: string;
45
- invite?: string | undefined;
46
45
  language?: string | undefined;
46
+ invite?: string | undefined;
47
47
  }, {
48
48
  code: string;
49
49
  state: string;
50
50
  state_cookie: string;
51
- invite?: string | undefined;
52
51
  language?: string | undefined;
52
+ invite?: string | undefined;
53
53
  }>;
54
54
  refresh: z.ZodObject<{
55
55
  token: z.ZodString;
@@ -0,0 +1,37 @@
1
+ import type { InjectionKey, Ref } from "vue";
2
+
3
+ const PAGE_FILL_HEIGHT_KEY: InjectionKey<Readonly<Ref<boolean>>> = Symbol(
4
+ "dms:page-fill-height",
5
+ );
6
+
7
+ /**
8
+ * The flex column a page filling the panel (`DefaultLayout({ fillHeight })`)
9
+ * is laid out in, from the scrolling panel body down to the component that
10
+ * takes the height left.
11
+ */
12
+ export const PAGE_FILL_HEIGHT_CLASSES = {
13
+ /**
14
+ * The page region, the panel body's only child. The minimum height sits
15
+ * here rather than on the component that fills: a floor set lower down
16
+ * overflows this region, and the panel then scrolls past its own bottom
17
+ * padding.
18
+ */
19
+ region: "flex min-h-[30rem] flex-1 flex-col",
20
+ /** The page stack, and the component wrapper that takes the height left. */
21
+ column: "flex min-h-0 flex-1 flex-col",
22
+ } as const;
23
+
24
+ /**
25
+ * Lets the page rendered in a layout's slot know whether the layout gives it
26
+ * the height of the panel.
27
+ */
28
+ export const providePageFillHeight = (fillHeight: Readonly<Ref<boolean>>) => {
29
+ provide(PAGE_FILL_HEIGHT_KEY, fillHeight);
30
+ };
31
+
32
+ /**
33
+ * Whether the layout rendering the page asked it to fill the panel height.
34
+ * `false` under a layout that does not provide it.
35
+ */
36
+ export const usePageFillHeight = (): Readonly<Ref<boolean>> =>
37
+ inject(PAGE_FILL_HEIGHT_KEY, () => ref(false), true);
@@ -8,6 +8,7 @@ import DashboardBanners from "../build/components/layout/DashboardBanners.vue";
8
8
  interface Props {
9
9
  fullWidth?: boolean;
10
10
  hideHeader?: boolean;
11
+ fillHeight?: boolean;
11
12
  icon?: string;
12
13
  title?: string;
13
14
  description?: string;
@@ -16,10 +17,17 @@ interface Props {
16
17
  const props = withDefaults(defineProps<Props>(), {
17
18
  fullWidth: true,
18
19
  hideHeader: false,
20
+ fillHeight: false,
19
21
  icon: "i-ph-file",
20
22
  title: undefined,
21
23
  description: undefined,
22
24
  });
25
+
26
+ providePageFillHeight(toRef(props, "fillHeight"));
27
+
28
+ const regionClass = computed(() =>
29
+ props.fillHeight ? PAGE_FILL_HEIGHT_CLASSES.region : undefined,
30
+ );
23
31
  </script>
24
32
 
25
33
  <template>
@@ -37,6 +45,7 @@ const props = withDefaults(defineProps<Props>(), {
37
45
  data-dms-page-region
38
46
  data-dms-page-content
39
47
  :full-width="props.fullWidth"
48
+ :class="regionClass"
40
49
  >
41
50
  <PageHeader
42
51
  v-if="props.title && !props.hideHeader"
@@ -339,6 +339,17 @@ const components = computed(() =>
339
339
  }),
340
340
  );
341
341
 
342
+ // A page filling the panel (`DefaultLayout({ fillHeight })`) gives the height
343
+ // left under its header to its last component; the ones above it keep theirs.
344
+ const fillHeight = usePageFillHeight();
345
+ const fillColumnClass = PAGE_FILL_HEIGHT_CLASSES.column;
346
+ const stackClass = computed(() =>
347
+ fillHeight.value ? fillColumnClass : undefined,
348
+ );
349
+ const fillingComponentId = computed(() =>
350
+ fillHeight.value ? components.value.at(-1)?.id : undefined,
351
+ );
352
+
342
353
  // --- Live-edit highlight ---------------------------------------------------
343
354
  // When the active page's layout changes in place (DMS dev hot-reload, same
344
355
  // route), animate the components that changed. Added/removed components are
@@ -457,12 +468,16 @@ if (import.meta.env.DEV) {
457
468
  tag="div"
458
469
  name="dms-fresh"
459
470
  class="dms-page-stack space-y-6"
471
+ :class="stackClass"
460
472
  :css="animateLayout"
461
473
  >
462
474
  <div
463
475
  v-for="component in components"
464
476
  :key="component.id"
465
- :class="{ 'dms-fresh-modified': freshComponentIds.has(component.id) }"
477
+ :class="[
478
+ { 'dms-fresh-modified': freshComponentIds.has(component.id) },
479
+ component.id === fillingComponentId && fillColumnClass,
480
+ ]"
466
481
  >
467
482
  <DmsRecursiveComponent
468
483
  :component="component"
@@ -10,6 +10,9 @@
10
10
  },
11
11
  "license": "Apache-2.0",
12
12
  "packageManager": "pnpm@10.6.5",
13
+ "engines": {
14
+ "@antelopejs/dms-frontend": ">=0.3.1 <0.4.0"
15
+ },
13
16
  "scripts": {
14
17
  "typecheck": "ajs dms verify-source --layer \"$PWD\" --local-package @antelopejs/dms=.. --local-package @antelopejs/interface-dms=../../interface-dms",
15
18
  "test": "vitest run",
@@ -58,7 +61,7 @@
58
61
  "zod": "^3.24.2"
59
62
  },
60
63
  "devDependencies": {
61
- "@antelopejs/core": ">=1.9.1 <2",
64
+ "@antelopejs/core": ">=1.10.1 <2",
62
65
  "@antelopejs/dms-frontend": ">=0.3.2 <1.0.0",
63
66
  "@eslint/js": "^8.57.0",
64
67
  "@inertiajs/vue3": "^3.7.0",
@@ -121,11 +121,11 @@ importers:
121
121
  version: 3.25.76
122
122
  devDependencies:
123
123
  '@antelopejs/core':
124
- specifier: '>=1.9.1 <2'
125
- version: 1.9.1(@types/node@24.8.1)
124
+ specifier: '>=1.10.1 <2'
125
+ version: 1.10.1(@types/node@24.8.1)
126
126
  '@antelopejs/dms-frontend':
127
127
  specifier: '>=0.3.2 <1.0.0'
128
- version: 0.3.2(@antelopejs/core@1.9.1(@types/node@24.8.1))
128
+ version: 0.3.2(@antelopejs/core@1.10.1(@types/node@24.8.1))
129
129
  '@eslint/js':
130
130
  specifier: ^8.57.0
131
131
  version: 8.57.0
@@ -196,8 +196,8 @@ packages:
196
196
  resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
197
197
  engines: {node: '>=10'}
198
198
 
199
- '@antelopejs/core@1.9.1':
200
- resolution: {integrity: sha512-MPLJak0rPluV4lgsdsO5ba03sgTxIGSncajBcoaoppuppQmksdpaemojIHiVLJ4LiG0iiH21Roaed8XXy1XkYQ==}
199
+ '@antelopejs/core@1.10.1':
200
+ resolution: {integrity: sha512-NcC6N8Aq14Ph7GRto0lpdRdgUKZAu3ZHDlAF5hXGFZPKKoNm8Da9i4nyB/NI4Sgx3rbE0/KoqCcHpTtQJgrAYg==}
201
201
  hasBin: true
202
202
 
203
203
  '@antelopejs/dms-frontend@0.3.2':
@@ -4312,7 +4312,7 @@ snapshots:
4312
4312
 
4313
4313
  '@alloc/quick-lru@5.2.0': {}
4314
4314
 
4315
- '@antelopejs/core@1.9.1(@types/node@24.8.1)':
4315
+ '@antelopejs/core@1.10.1(@types/node@24.8.1)':
4316
4316
  dependencies:
4317
4317
  '@antelopejs/interface-core': 0.1.1
4318
4318
  '@types/proper-lockfile': 4.1.4
@@ -4331,7 +4331,7 @@ snapshots:
4331
4331
  transitivePeerDependencies:
4332
4332
  - '@types/node'
4333
4333
 
4334
- '@antelopejs/dms-frontend@0.3.2(@antelopejs/core@1.9.1(@types/node@24.8.1))':
4334
+ '@antelopejs/dms-frontend@0.3.2(@antelopejs/core@1.10.1(@types/node@24.8.1))':
4335
4335
  dependencies:
4336
4336
  boxen: 5.1.2
4337
4337
  chalk: 4.1.2
@@ -4342,7 +4342,7 @@ snapshots:
4342
4342
  semver: 7.8.5
4343
4343
  unzipper: 0.12.5
4344
4344
  optionalDependencies:
4345
- '@antelopejs/core': 1.9.1(@types/node@24.8.1)
4345
+ '@antelopejs/core': 1.10.1(@types/node@24.8.1)
4346
4346
  '@oxc-parser/binding-darwin-arm64': 0.95.0
4347
4347
  '@oxc-parser/binding-darwin-x64': 0.95.0
4348
4348
  '@oxc-parser/binding-linux-arm64-gnu': 0.95.0
@@ -0,0 +1,352 @@
1
+ // @vitest-environment jsdom
2
+ import {
3
+ computed,
4
+ createApp,
5
+ createSSRApp,
6
+ h,
7
+ inject,
8
+ nextTick,
9
+ onMounted,
10
+ onUnmounted,
11
+ provide,
12
+ reactive,
13
+ ref,
14
+ Suspense,
15
+ toRef,
16
+ watch,
17
+ type App,
18
+ type Component,
19
+ type FunctionalComponent,
20
+ } from "vue";
21
+ import { renderToString } from "vue/server-renderer";
22
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
23
+ import DefaultLayout from "../layers/dms-layout/app/custom-layouts/DefaultLayout.vue";
24
+ import CatchAllPage from "../layers/dms-layout/app/pages/[...slug].vue";
25
+ import {
26
+ PAGE_FILL_HEIGHT_CLASSES,
27
+ providePageFillHeight,
28
+ usePageFillHeight,
29
+ } from "../layers/dms-layout/app/composables/layout/usePageFillHeight";
30
+
31
+ vi.mock(
32
+ "../layers/dms-layout/app/build/components/layout/DashboardSidebar.vue",
33
+ () => ({ default: () => null }),
34
+ );
35
+ vi.mock(
36
+ "../layers/dms-layout/app/build/components/layout/DashboardHeader.vue",
37
+ () => ({ default: () => null }),
38
+ );
39
+ vi.mock(
40
+ "../layers/dms-layout/app/build/components/layout/DashboardBanners.vue",
41
+ () => ({ default: () => null }),
42
+ );
43
+
44
+ interface PageLayoutFixture {
45
+ components: Record<string, { componentName: string }>;
46
+ }
47
+
48
+ const route = reactive({ path: "/tools/explorer", query: {} });
49
+ const pageLayout = ref<PageLayoutFixture | null>(null);
50
+
51
+ // The markup `DefaultLayout` and `pages/[...slug].vue` rendered for a
52
+ // two-component page before `fillHeight` existed, with the stubs below.
53
+ const FLOW_PAGE_MARKUP =
54
+ "<div data-dms-persistent-shell data-group><!---->" +
55
+ "<div data-panel><!--[--><!----><!----><!--]-->" +
56
+ "<div data-body>" +
57
+ '<div class="w-full max-w-none" data-dms-page-region data-dms-page-content><!--[-->' +
58
+ '<section class="flex gap-4 items-start pt-6 pb-7">' +
59
+ '<div class="rounded-lg bg-primary/10 shrink-0 ring ring-primary/20 flex items-center justify-center size-12"><i class="text-primary"></i></div>' +
60
+ '<div class="flex-1 min-w-0">' +
61
+ '<h1 class="text-highlighted text-[22px] font-semibold leading-tight tracking-tight"><!--[-->Explorer<!--]--></h1><!--v-if--></div><!--[--><!--]--></section><!--[-->' +
62
+ '<div class="dms-page-stack space-y-6">' +
63
+ '<div class="">' +
64
+ '<section data-component="stats" page-id="tools.explorer"></section></div>' +
65
+ '<div class="">' +
66
+ '<section data-component="explorer" page-id="tools.explorer"></section></div></div><!--]--><!--]--></div></div><!--[--><!----><!--]--></div></div>';
67
+
68
+ function withComponents(...ids: string[]): PageLayoutFixture {
69
+ return {
70
+ components: Object.fromEntries(
71
+ ids.map((id) => [id, { componentName: `tool-${id}` }]),
72
+ ),
73
+ };
74
+ }
75
+
76
+ // Functional stubs hand non-class attributes on only when told to: these two
77
+ // spread them, so the markers the layout sets stay in the markup.
78
+ const Group: FunctionalComponent = (_, { attrs, slots }) =>
79
+ h("div", { ...attrs, "data-group": "" }, slots.default?.());
80
+ Group.inheritAttrs = false;
81
+
82
+ const Panel: FunctionalComponent = (_, { slots }) =>
83
+ h("div", { "data-panel": "" }, [
84
+ slots.header?.(),
85
+ h("div", { "data-body": "" }, slots.body?.()),
86
+ slots.footer?.(),
87
+ ]);
88
+
89
+ // Nuxt UI merges its container classes with the ones it is given; the stub
90
+ // keeps the given ones next to one of its own, so a lost class shows.
91
+ const Container: FunctionalComponent = (_, { attrs, slots }) =>
92
+ h("div", { ...attrs, class: ["w-full", attrs.class] }, slots.default?.());
93
+ Container.inheritAttrs = false;
94
+
95
+ const PageComponent: FunctionalComponent<{ componentId: string }> = (props) =>
96
+ h("section", { "data-component": props.componentId });
97
+ PageComponent.props = ["componentId"];
98
+
99
+ const FillHeightProbe: FunctionalComponent = () =>
100
+ h("output", String(usePageFillHeight().value));
101
+
102
+ function installRuntime(): void {
103
+ const runtime: Record<string, unknown> = {
104
+ computed,
105
+ inject,
106
+ onMounted,
107
+ onUnmounted,
108
+ provide,
109
+ ref,
110
+ toRef,
111
+ watch,
112
+ HTTP_FORBIDDEN: 403,
113
+ HTTP_INTERNAL_SERVER_ERROR: 500,
114
+ HTTP_NOT_FOUND: 404,
115
+ PAGE_FILL_HEIGHT_CLASSES,
116
+ providePageFillHeight,
117
+ usePageFillHeight,
118
+ createError: vi.fn(),
119
+ defineDmsPageMeta: vi.fn(),
120
+ firstAccessiblePagePath: vi.fn(),
121
+ navigateDms: vi.fn(),
122
+ preloadPageLayoutComponents: vi.fn(),
123
+ providePageRealtime: vi.fn(),
124
+ resolveDmsComponent: () => "div",
125
+ runCustomValidation: vi.fn(),
126
+ showError: vi.fn(),
127
+ validateRequiredQueryParams: vi.fn(),
128
+ useDefinedFunctions: () => ({ getFunction: () => undefined }),
129
+ useDevReloading: () => ref(false),
130
+ useDmsRoute: () => route,
131
+ useHomepage: () => "/",
132
+ usePageLayout: async () => ({ pageLayout }),
133
+ usePageRealtime: () => ({}),
134
+ usePermissions: () => ({
135
+ permissions: ref([]),
136
+ refreshIfStale: async () => {},
137
+ }),
138
+ useSessionRecovery: () => ({
139
+ loggedIn: ref(true),
140
+ reconcileSession: async () => {},
141
+ redirectToAuth: async () => {},
142
+ }),
143
+ useSiteLayout: () => ({
144
+ siteLayout: ref({ pages: {} }),
145
+ siteLayoutTree: ref([]),
146
+ loadingError: ref(null),
147
+ loadSiteLayout: async () => {},
148
+ refresh: async () => {},
149
+ refreshIfStale: async () => {},
150
+ findMatchingRoute: () => ({
151
+ metadata: { fullId: "tools.explorer", hasAccess: true },
152
+ params: {},
153
+ }),
154
+ }),
155
+ useTranslation: () => ({ processI18n: (value: string) => value }),
156
+ };
157
+ Object.entries(runtime).forEach(([name, value]) =>
158
+ vi.stubGlobal(name, value),
159
+ );
160
+ }
161
+
162
+ function withStubs(app: App): App {
163
+ app.component("UDashboardGroup", Group);
164
+ app.component("UDashboardPanel", Panel);
165
+ app.component("UContainer", Container);
166
+ app.component("UIcon", () => h("i"));
167
+ app.component("DmsAppWidgetsDock", () => null);
168
+ app.component("DmsRecursiveComponent", PageComponent);
169
+ return app;
170
+ }
171
+
172
+ function renderInLayout(
173
+ layoutProps: Record<string, unknown>,
174
+ page: Component,
175
+ ): Promise<string> {
176
+ const root = () =>
177
+ h(
178
+ DefaultLayout,
179
+ { title: "Explorer", ...layoutProps },
180
+ { default: () => h(page) },
181
+ );
182
+ return renderToString(withStubs(createSSRApp(root)));
183
+ }
184
+
185
+ function parse(markup: string): HTMLElement {
186
+ const host = document.createElement("div");
187
+ host.innerHTML = markup;
188
+ return host;
189
+ }
190
+
191
+ function classesOf(element: Element | null | undefined): string[] {
192
+ return element?.getAttribute("class")?.split(" ").filter(Boolean) ?? [];
193
+ }
194
+
195
+ function expectClasses(element: Element | null | undefined, classes: string) {
196
+ expect(classesOf(element)).toEqual(
197
+ expect.arrayContaining(classes.split(" ")),
198
+ );
199
+ }
200
+
201
+ function expectNoFillClasses(element: Element | null | undefined) {
202
+ const fillClasses = new Set(
203
+ `${PAGE_FILL_HEIGHT_CLASSES.region} ${PAGE_FILL_HEIGHT_CLASSES.column}`.split(
204
+ " ",
205
+ ),
206
+ );
207
+ expect(classesOf(element).filter((name) => fillClasses.has(name))).toEqual(
208
+ [],
209
+ );
210
+ }
211
+
212
+ beforeEach(() => {
213
+ installRuntime();
214
+ route.path = "/tools/explorer";
215
+ pageLayout.value = withComponents("stats", "explorer");
216
+ });
217
+
218
+ afterEach(() => {
219
+ vi.unstubAllGlobals();
220
+ });
221
+
222
+ describe("usePageFillHeight", () => {
223
+ it("reads false under a layout that does not provide it", async () => {
224
+ const markup = await renderToString(createSSRApp(FillHeightProbe));
225
+ expect(markup).toBe("<output>false</output>");
226
+ });
227
+
228
+ it("follows the layout that provides it", async () => {
229
+ const fillHeight = ref(true);
230
+ const app = withStubs(
231
+ createApp(() =>
232
+ h(
233
+ DefaultLayout,
234
+ { fillHeight: fillHeight.value },
235
+ { default: () => h(FillHeightProbe) },
236
+ ),
237
+ ),
238
+ );
239
+ const host = document.createElement("div");
240
+ app.mount(host);
241
+ expect(host.querySelector("output")?.textContent).toBe("true");
242
+ fillHeight.value = false;
243
+ await nextTick();
244
+ expect(host.querySelector("output")?.textContent).toBe("false");
245
+ app.unmount();
246
+ });
247
+ });
248
+
249
+ describe("DefaultLayout without fillHeight", () => {
250
+ it("renders the page exactly as it did before the option existed", async () => {
251
+ expect(await renderInLayout({}, CatchAllPage)).toBe(FLOW_PAGE_MARKUP);
252
+ });
253
+
254
+ it("tells the page it does not fill the panel", async () => {
255
+ const markup = await renderInLayout({}, FillHeightProbe);
256
+ expect(parse(markup).querySelector("output")?.textContent).toBe("false");
257
+ });
258
+ });
259
+
260
+ describe("DefaultLayout with fillHeight", () => {
261
+ it("makes the page region a flex column with a minimum height", async () => {
262
+ const host = parse(
263
+ await renderInLayout({ fillHeight: true }, CatchAllPage),
264
+ );
265
+ const region = host.querySelector("[data-dms-page-content]");
266
+ expectClasses(region, PAGE_FILL_HEIGHT_CLASSES.region);
267
+ expectClasses(region, "w-full max-w-none");
268
+ });
269
+
270
+ it("keeps the page header a plain item of that column", async () => {
271
+ const host = parse(
272
+ await renderInLayout({ fillHeight: true }, CatchAllPage),
273
+ );
274
+ const header = host.querySelector("[data-dms-page-content] > section");
275
+ expect(classesOf(header)).toEqual([
276
+ "flex",
277
+ "gap-4",
278
+ "items-start",
279
+ "pt-6",
280
+ "pb-7",
281
+ ]);
282
+ });
283
+
284
+ it("gives the height left to the last component only", async () => {
285
+ const host = parse(
286
+ await renderInLayout({ fillHeight: true }, CatchAllPage),
287
+ );
288
+ const stack = host.querySelector(".dms-page-stack");
289
+ const [stats, explorer] = Array.from(stack?.children ?? []);
290
+ expectClasses(
291
+ stack,
292
+ `dms-page-stack space-y-6 ${PAGE_FILL_HEIGHT_CLASSES.column}`,
293
+ );
294
+ expectNoFillClasses(stats);
295
+ expectClasses(explorer, PAGE_FILL_HEIGHT_CLASSES.column);
296
+ expect(
297
+ explorer
298
+ ?.querySelector("[data-component]")
299
+ ?.getAttribute("data-component"),
300
+ ).toBe("explorer");
301
+ });
302
+
303
+ it("fills with the only component of a single-component page", async () => {
304
+ pageLayout.value = withComponents("explorer");
305
+ const host = parse(
306
+ await renderInLayout({ fillHeight: true }, CatchAllPage),
307
+ );
308
+ const wrappers = host.querySelectorAll(".dms-page-stack > div");
309
+ expect(wrappers).toHaveLength(1);
310
+ expectClasses(wrappers[0], PAGE_FILL_HEIGHT_CLASSES.column);
311
+ });
312
+
313
+ it("tells a custom page, rendered straight in the slot, to fill", async () => {
314
+ const markup = await renderInLayout({ fillHeight: true }, FillHeightProbe);
315
+ expect(parse(markup).querySelector("output")?.textContent).toBe("true");
316
+ });
317
+
318
+ it("drops the fill classes when a navigation lands on a page without the option", async () => {
319
+ const fillHeight = ref(true);
320
+ const app = withStubs(
321
+ createApp(() =>
322
+ h(
323
+ DefaultLayout,
324
+ { title: "Explorer", fillHeight: fillHeight.value },
325
+ {
326
+ default: () =>
327
+ h(Suspense, null, { default: () => h(CatchAllPage) }),
328
+ },
329
+ ),
330
+ ),
331
+ );
332
+ const host = document.createElement("div");
333
+ app.mount(host);
334
+ await vi.waitFor(() =>
335
+ expect(host.querySelector(".dms-page-stack")).not.toBeNull(),
336
+ );
337
+ expectClasses(
338
+ host.querySelector(".dms-page-stack"),
339
+ PAGE_FILL_HEIGHT_CLASSES.column,
340
+ );
341
+
342
+ fillHeight.value = false;
343
+ await nextTick();
344
+
345
+ expectNoFillClasses(host.querySelector("[data-dms-page-content]"));
346
+ expectNoFillClasses(host.querySelector(".dms-page-stack"));
347
+ host
348
+ .querySelectorAll(".dms-page-stack > div")
349
+ .forEach((wrapper) => expectNoFillClasses(wrapper));
350
+ app.unmount();
351
+ });
352
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@antelopejs/dms",
3
- "version": "0.4.8",
3
+ "version": "0.5.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/AntelopeJS/dms.git"
@@ -36,7 +36,7 @@
36
36
  "@antelopejs/interface-data-api": ">=0.1.9 <1.0.0",
37
37
  "@antelopejs/interface-database": ">=0.1.8 <1.0.0",
38
38
  "@antelopejs/interface-database-decorators": ">=0.1.7 <1.0.0",
39
- "@antelopejs/interface-dms": ">=0.2.6 <1.0.0",
39
+ "@antelopejs/interface-dms": ">=0.3.1 <0.4.0",
40
40
  "@antelopejs/interface-email": ">=0.0.5 <1.0.0",
41
41
  "@antelopejs/interface-file-storage": ">=0.1.3 <1.0.0",
42
42
  "archiver": "^7.0.1",
@@ -52,7 +52,7 @@
52
52
  "zod": "~3.24.2"
53
53
  },
54
54
  "devDependencies": {
55
- "@antelopejs/core": ">=1.9.1 <2",
55
+ "@antelopejs/core": ">=1.10.1 <2",
56
56
  "@antelopejs/tooling-configs": ">=0.0.6 <1.0.0",
57
57
  "@types/archiver": "^6.0.4",
58
58
  "@types/chai": "^4.3.20",
@@ -94,7 +94,7 @@
94
94
  "@antelopejs/interface-redis": ">=0.0.8 <1.0.0"
95
95
  },
96
96
  "peerDependencies": {
97
- "@antelopejs/core": ">=1.9.1 <2"
97
+ "@antelopejs/core": ">=1.10.1 <2"
98
98
  },
99
99
  "peerDependenciesMeta": {
100
100
  "@antelopejs/core": {
@@ -19,7 +19,8 @@ first-party modules follow `dms-<name>` purely for discoverability.
19
19
  pages/ # @RegisterPage classes + category.ts (see dms-pages)
20
20
  routes/ # HTTP controllers feeding the components
21
21
  implementations/<name>/index.ts # implementation of the module's own interface
22
- frontend-vue/ # Vue module: dms.frontend.ts, app/, i18n/locales/
22
+ frontend-vue/ # Vue module: dms.frontend.ts, app/, i18n/locales/,
23
+ # package.json (engines: the loader releases it supports)
23
24
  package.json / tsconfig.json
24
25
  interface-<name>/ # the module's OWN interface, its own published package
25
26
  src/index.ts # (rare — many modules ship none)
@@ -77,6 +77,11 @@ export function destroy(): void { /* deactivate subscriptions */ }
77
77
  register — it returns the module's root `CategoryInfo`. `AddFrontendModule` is what surfaces the
78
78
  module's frontend; without it the module is backend-only.
79
79
 
80
+ `renderer` names the framework (Vue 3), not a loader release. The `@antelopejs/dms-frontend`
81
+ releases the frontend module runs on go in the frontend module's own `package.json`, as
82
+ `"engines": { "@antelopejs/dms-frontend": ">=0.3.2 <0.4.0" }`: the loader refuses the module
83
+ outside that range. Cap it below the next loader minor while the loader is 0.x.
84
+
80
85
  ## Navigation & access — module pages are owner-only
81
86
 
82
87
  Three constraints are enforced at registration: a page's `category` must **descend from the