create-aiaui-module 0.1.0 → 0.1.2-TEST

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 (33) hide show
  1. package/README.md +84 -17
  2. package/bin/create-aiaui-module.js +86 -22
  3. package/package.json +23 -6
  4. package/runtime/__init__.py +71 -28
  5. package/runtime/_runtime/host.py +7 -15
  6. package/templates/HelloWorld/README.md +9 -1
  7. package/templates/JobInformation/CUSTOMIZATION.md +98 -148
  8. package/templates/JobInformation/README.md +17 -3
  9. package/templates/JobInformation/api.py +64 -20
  10. package/templates/JobInformation/resources/dist/modules.js +1548 -954
  11. package/templates/JobInformation/resources/src/contract.ts +109 -37
  12. package/templates/JobInformation/resources/src/customization.ts +15 -10
  13. package/templates/JobInformation/resources/src/defaults/DefaultPanel.vue +122 -15
  14. package/templates/JobInformation/resources/src/defaults/Icons.vue +49 -0
  15. package/templates/JobInformation/resources/src/defaults/InputCarriers.vue +12 -9
  16. package/templates/JobInformation/resources/src/defaults/JobBottom.vue +32 -0
  17. package/templates/JobInformation/resources/src/defaults/JobCard.vue +48 -0
  18. package/templates/JobInformation/resources/src/defaults/JobHeader.vue +41 -0
  19. package/templates/JobInformation/resources/src/defaults/JobMiddle.vue +41 -0
  20. package/templates/JobInformation/resources/src/defaults/JobName.vue +9 -0
  21. package/templates/JobInformation/resources/src/defaults/LotId.vue +14 -0
  22. package/templates/JobInformation/resources/src/defaults/OutputCarriers.vue +15 -8
  23. package/templates/JobInformation/resources/src/defaults/Recipe.vue +6 -12
  24. package/templates/JobInformation/resources/src/defaults/Status.vue +29 -0
  25. package/templates/JobInformation/resources/src/defaults/Timeline.vue +57 -14
  26. package/templates/JobInformation/resources/src/defaults/board-data.ts +36 -0
  27. package/templates/JobInformation/resources/src/defaults/config-defaults.ts +36 -0
  28. package/templates/JobInformation/resources/src/defaults/format.ts +51 -0
  29. package/templates/JobInformation/resources/src/defaults/slots.ts +16 -6
  30. package/templates/JobInformation/resources/src/defaults/styles.css +576 -47
  31. package/templates/JobInformation/resources/src/index.ts +10 -18
  32. package/templates/JobInformation/resources/src/bus.ts +0 -38
  33. package/templates/JobInformation/resources/src/defaults/Header.vue +0 -12
@@ -8,34 +8,115 @@ export type ModuleApi = {
8
8
  ): Promise<T>;
9
9
  };
10
10
 
11
- /** Bus listener signature. Payload is deliberately loose; type it at the use site. */
12
- export type ModuleBusListener = (payload?: any) => void;
11
+ /* -------------------------------------------------------------------------- */
12
+ /* Slot names */
13
+ /* -------------------------------------------------------------------------- */
13
14
 
14
15
  /**
15
- * A tiny communication bus shared by every slot component of one mount.
16
+ * Every customizable slot of a job card. Leaves override a single piece;
17
+ * region slots override the whole region (leaf overrides then only apply
18
+ * inside the platform's default regions). Replaceable top to bottom:
16
19
  *
17
- * - `emit` / `on` / `off` — decoupled events between slots
18
- * - `expose` / `peer` — imperative handles published by one slot and called by another
19
- *
20
- * The bus is created per `mount` by `index.ts`. It never crosses module or
21
- * Instance boundaries: switching an Instance mounts a fresh bus.
20
+ * board the whole job card
21
+ * ├── headerRegion job name + lot id + status (slot.board also
22
+ * │ ├── jobName, lotId, status replaces the row)
23
+ * ├── middleRegion input carriers + timeline + output carriers
24
+ * │ └── inputCarriers, timeline, outputCarriers
25
+ * └── bottomRegion recipe area + footer (slot.board also
26
+ * └── recipe replaces footer)
27
+ */
28
+ export const SlotKey = {
29
+ board: "board",
30
+ headerRegion: "headerRegion",
31
+ jobName: "jobName",
32
+ lotId: "lotId",
33
+ status: "status",
34
+ middleRegion: "middleRegion",
35
+ inputCarriers: "inputCarriers",
36
+ timeline: "timeline",
37
+ outputCarriers: "outputCarriers",
38
+ bottomRegion: "bottomRegion",
39
+ recipe: "recipe",
40
+ } as const;
41
+
42
+ export type SlotKeyType = (typeof SlotKey)[keyof typeof SlotKey];
43
+
44
+ /** Any Vue component that renders one slot. Custom components receive `ModuleSlotProps`. */
45
+ export type SlotComponent = Component;
46
+
47
+ export type SlotMap = Record<string, SlotComponent>;
48
+
49
+ /* -------------------------------------------------------------------------- */
50
+ /* Default panel data contract */
51
+ /* -------------------------------------------------------------------------- */
52
+
53
+ /** One carrier entry rendered by the carrier slots. */
54
+ export type ModuleCarrier = { code: string };
55
+
56
+ /** One step of a job's timeline (rendered by the default `timeline` slot). */
57
+ export type ModuleTimelineStep = {
58
+ label: string;
59
+ state?: "done" | "active" | "pending";
60
+ /** ISO timestamp; durations between steps are computed client-side. */
61
+ at?: string | null;
62
+ };
63
+
64
+ /**
65
+ * One job entry returned by `GET /jobs` (the module's default data contract).
66
+ * Every item is rendered as one job card (the `board` slot).
67
+ */
68
+ export type ModuleJobRecord = {
69
+ id: string;
70
+ name: string;
71
+ lot?: string | null;
72
+ /** State name; matched against `config.states` for its color and icon. */
73
+ state?: string | null;
74
+ carriers?: { input?: ModuleCarrier[]; output?: ModuleCarrier[] };
75
+ timeline?: ModuleTimelineStep[];
76
+ recipe?: string | null;
77
+ };
78
+
79
+ /** Response of `GET /jobs` consumed by the default panel. */
80
+ export type ModuleJobPage = {
81
+ items: ModuleJobRecord[];
82
+ total: number;
83
+ page?: number;
84
+ pageSize?: number;
85
+ };
86
+
87
+ /** One state entry returned by `GET /config`. */
88
+ export type ModuleStateVisual = {
89
+ name: string;
90
+ color?: string | null;
91
+ icon?: string | null;
92
+ };
93
+
94
+ /**
95
+ * Response of `GET /config` (the module's configuration contract).
96
+ * Every field is optional; any missing field falls back to the template's
97
+ * built-in defaults (all parts visible, default state colors).
22
98
  */
23
- export type ModuleBus = {
24
- emit(event: string, payload?: unknown): void;
25
- on(event: string, listener: ModuleBusListener): void;
26
- off(event: string, listener: ModuleBusListener): void;
27
- expose<T extends object>(name: string, handle: T): void;
28
- release(name: string): void;
29
- peer<T extends object>(name: string): T | undefined;
99
+ export type ModuleConfig = {
100
+ display?: {
101
+ lotId?: boolean;
102
+ recipe?: boolean;
103
+ inputCarriers?: boolean;
104
+ outputCarriers?: boolean;
105
+ timeline?: boolean;
106
+ };
107
+ /** State visuals for `<slot>status</slot>`; missing entries use the built-in token map. */
108
+ states?: ModuleStateVisual[];
30
109
  };
31
110
 
111
+ /* -------------------------------------------------------------------------- */
112
+ /* Context & customization */
113
+ /* -------------------------------------------------------------------------- */
114
+
32
115
  export type ModuleContext = {
33
116
  module: string;
34
117
  instanceId: string;
35
118
  api: ModuleApi;
36
119
  signal: AbortSignal;
37
- /** Sibling-slot communication; created per mount. See CUSTOMIZATION.md. */
38
- bus: ModuleBus;
39
120
  place: (spec: {
40
121
  kind: "route" | "slot";
41
122
  path?: string;
@@ -46,29 +127,20 @@ export type ModuleContext = {
46
127
  };
47
128
 
48
129
  /**
49
- * What the host platform actually passes into `mount`. The module enriches
50
- * it with `bus` before slot components ever see it; slots always receive the
51
- * full `ModuleContext` (with `bus`).
130
+ * What the host platform actually passes into `mount`. The module owns everything
131
+ * outside this context and applies it to slots itself.
52
132
  */
53
- export type HostModuleContext = Omit<ModuleContext, "bus">;
133
+ export type HostModuleContext = ModuleContext;
54
134
 
55
135
  /**
56
- * A replaceable UI box. Slot components receive `context` as their only prop,
57
- * so a developer can swap any box while keeping access to the module API.
136
+ * Props every slot component receives: the module context, the per-job record
137
+ * being rendered, and the merged module configuration.
58
138
  */
59
- export type SlotComponent = Component;
60
-
61
- export type SlotMap = Record<string, SlotComponent>;
62
-
63
- export type SlotCustomization = {
64
- mode?: "slots";
65
- slots?: SlotMap;
66
- };
67
-
68
- export type FullCustomization = {
69
- mode: "full";
70
- mount: (container: HTMLElement, context: ModuleContext) => void | Promise<void>;
71
- unmount: () => void;
139
+ export type ModuleSlotProps = {
140
+ context: ModuleContext;
141
+ job: ModuleJobRecord;
142
+ config: ModuleConfig;
72
143
  };
73
144
 
74
- export type Customization = SlotCustomization | FullCustomization;
145
+ /** A slot override map. Editing `resources/src/customization.ts` swaps these in. */
146
+ export type Customization = { slots: SlotMap };
@@ -3,23 +3,28 @@ import type { Customization } from "./contract";
3
3
  /**
4
4
  * Developer customization entry point.
5
5
  *
6
- * Default (no change): keeps the built-in panel.
6
+ * Default (no change): keeps the built-in job card layout.
7
7
  *
8
- * Partial (replace individual boxes):
9
- * import MyHeader from "./custom/MyHeader.vue";
8
+ * Replace one piece inside a card (layout stays platform-owned):
9
+ * import MyJobName from "./custom/MyJobName.vue";
10
10
  * export const customization: Customization = {
11
- * mode: "slots",
12
- * slots: { header: MyHeader },
11
+ * slots: { jobName: MyJobName },
13
12
  * };
14
13
  *
15
- * Full (replace the entire module):
14
+ * Replace a whole region or the whole card:
15
+ * import MyMiddle from "./custom/MyMiddle.vue";
16
16
  * export const customization: Customization = {
17
- * mode: "full",
18
- * mount(container, context) { ... },
19
- * unmount() { ... },
17
+ * slots: { middleRegion: MyMiddle }, // the three columns region
20
18
  * };
19
+ * export const customization: Customization = {
20
+ * slots: { board: MyBoard }, // the entire single-job card
21
+ * };
22
+ *
23
+ * Every custom component receives `{ context, job, config }` as props
24
+ * (see `contract.ts` for the slot table and CUSTOMIZATION.md for the API/data
25
+ * contract). The platform, batch listing, `GET /jobs`/`GET /config` and card
26
+ * frame stay under AIAUI control regardless of which slot you replace.
21
27
  */
22
28
  export const customization: Customization = {
23
- mode: "slots",
24
29
  slots: {},
25
30
  };
@@ -1,22 +1,129 @@
1
1
  <script setup lang="ts">
2
- import type { Component } from "vue";
3
- import type { ModuleContext } from "../contract";
4
-
5
- const props = defineProps<{
6
- context: ModuleContext;
7
- slots: Record<string, Component>;
8
- slotOrder: string[];
9
- }>();
2
+ import { computed, onMounted, ref } from "vue";
3
+ import type {
4
+ ModuleConfig,
5
+ ModuleContext,
6
+ ModuleJobPage,
7
+ SlotMap,
8
+ } from "../contract";
9
+ import { mergeConfig } from "./config-defaults";
10
+ import { DEMO_JOBS } from "./board-data";
11
+ import { defaultSlots } from "./slots";
12
+ import { customization } from "../customization";
13
+ import JobCard from "./JobCard.vue";
14
+ import Icons from "./Icons.vue";
15
+
16
+ const props = defineProps<{ context: ModuleContext; slotMap?: SlotMap }>();
17
+
18
+ const slotMap = computed(() =>
19
+ props.slotMap ?? { ...defaultSlots, ...(customization.slots ?? {}) },
20
+ );
21
+
22
+ const jobs = ref<ModuleJobPage["items"]>(DEMO_JOBS);
23
+ const config = ref<ModuleConfig>(mergeConfig(null));
24
+ const note = ref("");
25
+ const search = ref("");
26
+ const stateFilter = ref("");
27
+ const loading = ref(true);
28
+
29
+ async function loadConfig() {
30
+ const signal = props.context.signal;
31
+ try {
32
+ const raw = await props.context.api.call<ModuleConfig>("GET", "config");
33
+ if (signal.aborted) return;
34
+ config.value = mergeConfig(raw);
35
+ } catch {
36
+ if (signal.aborted) return;
37
+ // keep whatever config we have (defaults on first mount)
38
+ }
39
+ }
40
+
41
+ async function loadJobs() {
42
+ const signal = props.context.signal;
43
+ loading.value = true;
44
+ try {
45
+ const page = await props.context.api.call<ModuleJobPage>("GET", "jobs", {
46
+ query: { page: 1, pageSize: 10 },
47
+ });
48
+ if (signal.aborted) return;
49
+ jobs.value = page.items;
50
+ note.value = "";
51
+ } catch {
52
+ if (signal.aborted) return;
53
+ jobs.value = DEMO_JOBS;
54
+ note.value = "GET /jobs unavailable — showing demo data";
55
+ } finally {
56
+ if (!signal.aborted) loading.value = false;
57
+ }
58
+ }
59
+
60
+ async function refresh() {
61
+ await Promise.all([loadConfig(), loadJobs()]);
62
+ }
63
+
64
+ onMounted(refresh);
65
+
66
+ const states = computed(() =>
67
+ Array.from(new Set(jobs.value.map((job) => job.state ?? "—"))),
68
+ );
69
+
70
+ const visibleJobs = computed(() =>
71
+ jobs.value.filter((job) => {
72
+ const matchesName =
73
+ !search.value.trim() ||
74
+ job.name.toLowerCase().includes(search.value.trim().toLowerCase());
75
+ const matchesState =
76
+ !stateFilter.value || (job.state ?? "—") === stateFilter.value;
77
+ return matchesName && matchesState;
78
+ }),
79
+ );
10
80
  </script>
11
81
 
12
82
  <template>
13
- <main class="aia-job-information">
14
- <component
15
- v-for="name in props.slotOrder"
16
- v-if="props.slots[name]"
17
- :key="name"
18
- :is="props.slots[name]"
83
+ <section class="legacy-jobs" aria-label="Job Information">
84
+ <header class="legacy-toolbar">
85
+ <div class="legacy-filters">
86
+ <label class="legacy-search">
87
+ <Icons name="search" :size="14" />
88
+ <input
89
+ v-model="search"
90
+ placeholder="Search jobs"
91
+ type="text"
92
+ aria-label="Search jobs"
93
+ />
94
+ </label>
95
+ <select
96
+ v-model="stateFilter"
97
+ class="legacy-filter"
98
+ aria-label="Status filter"
99
+ >
100
+ <option value="">All statuses</option>
101
+ <option v-for="state in states" :key="state" :value="state">
102
+ {{ state }}
103
+ </option>
104
+ </select>
105
+ <button
106
+ class="legacy-refresh"
107
+ type="button"
108
+ title="Refresh"
109
+ :aria-busy="loading"
110
+ @click="refresh"
111
+ >
112
+ <Icons name="refresh" :size="14" :class="{ 'aia-spinning': loading }" />
113
+ </button>
114
+ </div>
115
+ </header>
116
+ <p v-if="note" class="legacy-job-note" role="note">{{ note }}</p>
117
+ <JobCard
118
+ v-for="job in visibleJobs"
119
+ :key="job.id"
19
120
  :context="props.context"
121
+ :slotMap="slotMap"
122
+ :job="job"
123
+ :config="config"
20
124
  />
21
- </main>
125
+ <p v-if="visibleJobs.length === 0" class="legacy-job-empty">
126
+ No jobs match the current filters.
127
+ </p>
128
+ </section>
22
129
  </template>
@@ -0,0 +1,49 @@
1
+ <script setup lang="ts">
2
+ /* Minimal inline icon set shared by the built-in slots (no external dependency). */
3
+
4
+ const props = defineProps<{
5
+ name:
6
+ | "clock"
7
+ | "list"
8
+ | "stop"
9
+ | "activity"
10
+ | "flag"
11
+ | "search"
12
+ | "refresh"
13
+ | "chevron-down"
14
+ | "package";
15
+ size?: number;
16
+ }>();
17
+
18
+ const paths: Record<string, string> = {
19
+ clock:
20
+ '<circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline>',
21
+ list: '<line x1="8" y1="6" x2="21" y2="6"></line><line x1="8" y1="12" x2="21" y2="12"></line><line x1="8" y1="18" x2="21" y2="18"></line><line x1="3" y1="6" x2="3.01" y2="6"></line><line x1="3" y1="12" x2="3.01" y2="12"></line><line x1="3" y1="18" x2="3.01" y2="18"></line>',
22
+ stop: '<circle cx="12" cy="12" r="10"></circle><rect x="9" y="9" width="6" height="6" rx="1"></rect>',
23
+ activity: '<path d="M22 12h-4l-3 9L9 3l-3 9H2"></path>',
24
+ flag:
25
+ '<path d="M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z"></path><line x1="4" y1="22" x2="4" y2="15"></line>',
26
+ search:
27
+ '<circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line>',
28
+ refresh:
29
+ '<polyline points="23 4 23 10 17 10"></polyline><polyline points="1 20 1 14 7 14"></polyline><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path>',
30
+ "chevron-down": '<polyline points="6 9 12 15 18 9"></polyline>',
31
+ package:
32
+ '<path d="m9 4c0-1.7 1.3-3 3-3s3 1.3 3 3v1h3.4c1.4 0 2.6 1.1 2.6 2.5v10c0 1.4-1.1 2.5-2.6 2.5H5.6C4.1 20 3 18.9 3 17.5v-10C3 6.1 4.1 5 5.6 5H9V4Z"></path><path d="M12 4v16"></path>',
33
+ };
34
+ </script>
35
+
36
+ <template>
37
+ <svg
38
+ :width="props.size ?? 16"
39
+ :height="props.size ?? 16"
40
+ viewBox="0 0 24 24"
41
+ fill="none"
42
+ stroke="currentColor"
43
+ stroke-width="2"
44
+ stroke-linecap="round"
45
+ stroke-linejoin="round"
46
+ aria-hidden="true"
47
+ v-html="paths[props.name]"
48
+ />
49
+ </template>
@@ -1,15 +1,18 @@
1
1
  <script setup lang="ts">
2
- import type { ModuleContext } from "../contract";
2
+ import type { ModuleSlotProps } from "../contract";
3
3
 
4
- defineProps<{ context: ModuleContext }>();
4
+ const props = defineProps<ModuleSlotProps>();
5
+ const carriers = props.job.carriers?.input ?? [];
5
6
  </script>
6
7
 
7
8
  <template>
8
- <section class="aiaji-slot aiaji-carriers aiaji-carriers-input">
9
- <h3>INPUT CARRIER(S)</h3>
10
- <ul>
11
- <li>CR-01</li>
12
- <li>CR-02</li>
13
- </ul>
14
- </section>
9
+ <aside class="legacy-carriers legacy-carriers-in" :aria-label="`Input carriers for ${job.name}`">
10
+ <span class="legacy-carriers-label">INPUT CARRIER(S)</span>
11
+ <div class="legacy-carriers-list">
12
+ <span v-for="carrier in carriers" :key="carrier.code" class="legacy-carrier-chip">{{
13
+ carrier.code
14
+ }}</span>
15
+ <span v-if="carriers.length === 0" class="legacy-carrier-empty">—</span>
16
+ </div>
17
+ </aside>
15
18
  </template>
@@ -0,0 +1,32 @@
1
+ <script setup lang="ts">
2
+ import { computed } from "vue";
3
+ import type {
4
+ ModuleConfig,
5
+ ModuleContext,
6
+ ModuleJobRecord,
7
+ SlotMap,
8
+ } from "../contract";
9
+ import Recipe from "./Recipe.vue";
10
+
11
+ const props = defineProps<{
12
+ context: ModuleContext;
13
+ job: ModuleJobRecord;
14
+ config: ModuleConfig;
15
+ slotMap?: SlotMap;
16
+ }>();
17
+
18
+ const leafProps = computed(() => ({
19
+ context: props.context,
20
+ job: props.job,
21
+ config: props.config,
22
+ }));
23
+
24
+ const showRecipe = computed(() => props.config.display?.recipe !== false);
25
+ const recipe = computed(() => (props.slotMap ?? {}).recipe ?? Recipe);
26
+ </script>
27
+
28
+ <template>
29
+ <div v-if="showRecipe" class="legacy-progress">
30
+ <component :is="recipe" v-bind="leafProps" />
31
+ </div>
32
+ </template>
@@ -0,0 +1,48 @@
1
+ <script setup lang="ts">
2
+ import { computed } from "vue";
3
+ import type { Component } from "vue";
4
+ import type {
5
+ ModuleConfig,
6
+ ModuleContext,
7
+ ModuleJobRecord,
8
+ SlotMap,
9
+ } from "../contract";
10
+ import JobHeader from "./JobHeader.vue";
11
+ import JobMiddle from "./JobMiddle.vue";
12
+ import JobBottom from "./JobBottom.vue";
13
+
14
+ /**
15
+ * Default `board` slot = one job card. Overriding `board` replaces the whole
16
+ * card (header, middle, bottom); leaf/region overrides then only apply when
17
+ * the platform's default board composes them.
18
+ */
19
+ const props = defineProps<{
20
+ context: ModuleContext;
21
+ job: ModuleJobRecord;
22
+ config: ModuleConfig;
23
+ slotMap?: SlotMap;
24
+ }>();
25
+
26
+ const board = computed(() => props.slotMap?.board ?? null);
27
+ const regionProps = computed(() => ({ context: props.context, job: props.job, config: props.config, slotMap: props.slotMap }));
28
+ const headerRegion = computed(() => props.slotMap?.headerRegion ?? JobHeader);
29
+ const middleRegion = computed(() => props.slotMap?.middleRegion ?? JobMiddle);
30
+ const bottomRegion = computed(() => props.slotMap?.bottomRegion ?? JobBottom);
31
+ </script>
32
+
33
+ <template>
34
+ <article class="legacy-card" :data-job-id="job.id">
35
+ <component
36
+ v-if="board"
37
+ :is="board as Component"
38
+ :context="context"
39
+ :job="job"
40
+ :config="config"
41
+ />
42
+ <template v-else>
43
+ <component :is="headerRegion" v-bind="regionProps" />
44
+ <component :is="middleRegion" v-bind="regionProps" />
45
+ <component :is="bottomRegion" v-bind="regionProps" />
46
+ </template>
47
+ </article>
48
+ </template>
@@ -0,0 +1,41 @@
1
+ <script setup lang="ts">
2
+ import { computed } from "vue";
3
+ import type {
4
+ ModuleConfig,
5
+ ModuleContext,
6
+ ModuleJobRecord,
7
+ SlotMap,
8
+ } from "../contract";
9
+ import JobName from "./JobName.vue";
10
+ import LotId from "./LotId.vue";
11
+ import Status from "./Status.vue";
12
+
13
+ const props = defineProps<{
14
+ context: ModuleContext;
15
+ job: ModuleJobRecord;
16
+ config: ModuleConfig;
17
+ slotMap?: SlotMap;
18
+ }>();
19
+
20
+ const leafProps = computed(() => ({
21
+ context: props.context,
22
+ job: props.job,
23
+ config: props.config,
24
+ }));
25
+
26
+ const jobName = computed(() => (props.slotMap ?? {}).jobName ?? JobName);
27
+ const lotId = computed(() =>
28
+ props.config.display?.lotId === false ? null : ((props.slotMap ?? {}).lotId ?? LotId),
29
+ );
30
+ const status = computed(() => (props.slotMap ?? {}).status ?? Status);
31
+ </script>
32
+
33
+ <template>
34
+ <div class="legacy-job-head">
35
+ <div class="legacy-job-main">
36
+ <component :is="jobName" v-bind="leafProps" />
37
+ <component v-if="lotId" :is="lotId" v-bind="leafProps" />
38
+ </div>
39
+ <component :is="status" v-bind="leafProps" />
40
+ </div>
41
+ </template>
@@ -0,0 +1,41 @@
1
+ <script setup lang="ts">
2
+ import { computed } from "vue";
3
+ import type {
4
+ ModuleConfig,
5
+ ModuleContext,
6
+ ModuleJobRecord,
7
+ SlotMap,
8
+ } from "../contract";
9
+ import InputCarriers from "./InputCarriers.vue";
10
+ import OutputCarriers from "./OutputCarriers.vue";
11
+ import Timeline from "./Timeline.vue";
12
+
13
+ const props = defineProps<{
14
+ context: ModuleContext;
15
+ job: ModuleJobRecord;
16
+ config: ModuleConfig;
17
+ slotMap?: SlotMap;
18
+ }>();
19
+
20
+ const leafProps = computed(() => ({
21
+ context: props.context,
22
+ job: props.job,
23
+ config: props.config,
24
+ }));
25
+
26
+ const showInput = computed(() => props.config.display?.inputCarriers !== false);
27
+ const showTimeline = computed(() => props.config.display?.timeline !== false);
28
+ const showOutput = computed(() => props.config.display?.outputCarriers !== false);
29
+
30
+ const inputCarriers = computed(() => (props.slotMap ?? {}).inputCarriers ?? InputCarriers);
31
+ const timeline = computed(() => (props.slotMap ?? {}).timeline ?? Timeline);
32
+ const outputCarriers = computed(() => (props.slotMap ?? {}).outputCarriers ?? OutputCarriers);
33
+ </script>
34
+
35
+ <template>
36
+ <div class="legacy-body">
37
+ <component v-if="showInput" :is="inputCarriers" v-bind="leafProps" />
38
+ <component v-if="showTimeline" :is="timeline" v-bind="leafProps" />
39
+ <component v-if="showOutput" :is="outputCarriers" v-bind="leafProps" />
40
+ </div>
41
+ </template>
@@ -0,0 +1,9 @@
1
+ <script setup lang="ts">
2
+ import type { ModuleSlotProps } from "../contract";
3
+
4
+ defineProps<ModuleSlotProps>();
5
+ </script>
6
+
7
+ <template>
8
+ <span class="legacy-job-name">{{ job.name }}</span>
9
+ </template>
@@ -0,0 +1,14 @@
1
+ <script setup lang="ts">
2
+ import type { ModuleSlotProps } from "../contract";
3
+ import Icons from "./Icons.vue";
4
+
5
+ defineProps<ModuleSlotProps>();
6
+ </script>
7
+
8
+ <template>
9
+ <span v-if="job.lot" class="legacy-lot" :title="`Lot ID · ${job.lot}`">
10
+ <Icons name="package" :size="12" />
11
+ <span class="legacy-lot-label">Lot ID</span>
12
+ <span class="legacy-lot-value">{{ job.lot }}</span>
13
+ </span>
14
+ </template>
@@ -1,14 +1,21 @@
1
1
  <script setup lang="ts">
2
- import type { ModuleContext } from "../contract";
2
+ import type { ModuleSlotProps } from "../contract";
3
3
 
4
- defineProps<{ context: ModuleContext }>();
4
+ const props = defineProps<ModuleSlotProps>();
5
+ const carriers = props.job.carriers?.output ?? [];
5
6
  </script>
6
7
 
7
8
  <template>
8
- <section class="aiaji-slot aiaji-carriers aiaji-carriers-output">
9
- <h3>OUTPUT CARRIER(S)</h3>
10
- <ul>
11
- <li>—</li>
12
- </ul>
13
- </section>
9
+ <aside
10
+ class="legacy-carriers legacy-carriers-out"
11
+ :aria-label="`Output carriers for ${job.name}`"
12
+ >
13
+ <span class="legacy-carriers-label">OUTPUT CARRIER(S)</span>
14
+ <div class="legacy-carriers-list">
15
+ <span v-for="carrier in carriers" :key="carrier.code" class="legacy-carrier-chip">{{
16
+ carrier.code
17
+ }}</span>
18
+ <span v-if="carriers.length === 0" class="legacy-carrier-empty">—</span>
19
+ </div>
20
+ </aside>
14
21
  </template>