@cosmicdrift/kumiko-headless 0.1.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.
@@ -0,0 +1,164 @@
1
+ import type {
2
+ EntityDefinition,
3
+ EntityEditScreenDefinition,
4
+ FieldCondition,
5
+ } from "@cosmicdrift/kumiko-framework/ui-types";
6
+ import { normalizeEditField, parseRefTarget } from "@cosmicdrift/kumiko-framework/ui-types";
7
+ import { buildOptionLabels, fieldLabelKey } from "./list";
8
+ import type { EditFieldViewModel, EditSectionViewModel, EditViewModel, Translate } from "./types";
9
+
10
+ export type ComputeEditViewModelInput<
11
+ TValues extends Readonly<Record<string, unknown>> = Readonly<Record<string, unknown>>,
12
+ TCtx = unknown,
13
+ > = {
14
+ readonly screen: EntityEditScreenDefinition;
15
+ readonly entity: EntityDefinition;
16
+ readonly values: TValues;
17
+ readonly translate: Translate;
18
+ readonly featureName: string;
19
+ // Optional condition context — forwarded to field visible/readonly/required
20
+ // predicates. Normally the host app passes `{ user, config, ... }`.
21
+ readonly ctx?: TCtx;
22
+ };
23
+
24
+ // Pure transform from screen-def + entity-def + row-values to the flat
25
+ // section/field tree the renderer draws. Conditional predicates are
26
+ // evaluated here so the renderer never re-runs them during React render.
27
+ //
28
+ // TValues / TCtx are propagated through evalCondition so call-sites can
29
+ // `computeEditViewModel<OrderRow, AdminCtx>(...)` and get typed data/ctx
30
+ // inside their predicates. Default `unknown` keeps unannotated call-sites
31
+ // working unchanged.
32
+ export function computeEditViewModel<
33
+ TValues extends Readonly<Record<string, unknown>> = Readonly<Record<string, unknown>>,
34
+ TCtx = unknown,
35
+ >(input: ComputeEditViewModelInput<TValues, TCtx>): EditViewModel {
36
+ const { screen, entity, values, translate, featureName, ctx } = input;
37
+
38
+ const sections: EditSectionViewModel[] = screen.layout.sections.map((sectionSpec) => {
39
+ const fields: EditFieldViewModel[] = sectionSpec.fields.map((fieldSpec) => {
40
+ const normalized = normalizeEditField(fieldSpec);
41
+ const fieldDef = entity.fields[normalized.field];
42
+ if (!fieldDef) {
43
+ throw new Error(
44
+ `computeEditViewModel: screen "${screen.id}" references unknown field "${normalized.field}" on entity "${screen.entity}"`,
45
+ );
46
+ }
47
+ const label = translate(fieldLabelKey(featureName, screen.entity, normalized.field));
48
+ const visible = evalCondition<TValues, TCtx>(normalized.visible, true, values, ctx);
49
+ // `readOnly` (camelCase) is the name on both sides: EditFieldSpec
50
+ // in the engine, and the view-model emitted here. One convention
51
+ // through the stack beats translating at the boundary.
52
+ const readOnly = evalCondition<TValues, TCtx>(normalized.readOnly, false, values, ctx);
53
+ // `required` on the field-spec overrides the entity-default. A
54
+ // field that's required at the entity-level but marked required:
55
+ // false on the screen (e.g. a soft-onboarding wizard that
56
+ // collects less up-front) respects the screen override.
57
+ const entityRequired = (fieldDef as unknown as { required?: boolean }).required === true;
58
+ const required = evalCondition<TValues, TCtx>(
59
+ normalized.required,
60
+ entityRequired,
61
+ values,
62
+ ctx,
63
+ );
64
+ // Select-Optionen bei `type: "select"` mitnehmen — der Renderer
65
+ // braucht sie für das Dropdown ohne nochmal die EntityDefinition
66
+ // zu reichen. Plus translated Labels (gleiche Convention wie der
67
+ // List-Builder), damit Form-Selects und List-Cells dieselbe
68
+ // i18n-Quelle teilen.
69
+ const options =
70
+ fieldDef.type === "select"
71
+ ? ((fieldDef as unknown as { options?: readonly string[] }).options ?? [])
72
+ : undefined;
73
+ const optionLabels =
74
+ options !== undefined
75
+ ? buildOptionLabels(translate, featureName, screen.entity, normalized.field, options)
76
+ : undefined;
77
+ // Multiline-Hint bei `type: "text"` — der Renderer wechselt
78
+ // dann auf textarea. ViewModel hält die Form-Render-Decision
79
+ // damit der Renderer nicht selbst auf die FieldDefinition greift.
80
+ const multiline =
81
+ fieldDef.type === "text"
82
+ ? (fieldDef as unknown as { multiline?: boolean | { rows?: number } }).multiline
83
+ : undefined;
84
+ // Tier 2.7e-3: Reference-Field — refEntity + refLabelField in
85
+ // das ViewModel reichen damit der Renderer die Lookup-Query
86
+ // bauen kann ohne noch an EntityDefinition zu greifen.
87
+ // Tier 2.7e-3: Reference-Field — entity-String kann same-feature
88
+ // ("user") oder cross-feature ("users:user") sein. parseRefTarget
89
+ // splittet das, der Renderer baut die Lookup-QN aus
90
+ // (refFeature, refEntity).
91
+ const refRaw =
92
+ fieldDef.type === "reference"
93
+ ? (fieldDef as unknown as { entity?: string }).entity
94
+ : undefined;
95
+ const refTarget = refRaw !== undefined ? parseRefTarget(refRaw, featureName) : undefined;
96
+ const refEntity = refTarget?.entityName;
97
+ const refFeature = refTarget?.featureName;
98
+ const refLabelField =
99
+ fieldDef.type === "reference"
100
+ ? ((fieldDef as unknown as { labelField?: string }).labelField ?? "id")
101
+ : undefined;
102
+ const refMultiple =
103
+ fieldDef.type === "reference"
104
+ ? ((fieldDef as unknown as { multiple?: boolean }).multiple ?? false)
105
+ : undefined;
106
+ const view: EditFieldViewModel = {
107
+ field: normalized.field,
108
+ label,
109
+ type: fieldDef.type,
110
+ value: values[normalized.field],
111
+ visible,
112
+ readOnly,
113
+ required,
114
+ ...(normalized.span !== undefined && { span: normalized.span }),
115
+ ...(normalized.renderer !== undefined && { renderer: normalized.renderer }),
116
+ ...(options !== undefined && { options }),
117
+ ...(optionLabels !== undefined && { optionLabels }),
118
+ ...(multiline !== undefined && { multiline }),
119
+ ...(refEntity !== undefined && { refEntity }),
120
+ ...(refFeature !== undefined && { refFeature }),
121
+ ...(refLabelField !== undefined && { refLabelField }),
122
+ ...(refMultiple !== undefined && { refMultiple }),
123
+ };
124
+ return view;
125
+ });
126
+ return {
127
+ title: translate(sectionSpec.title),
128
+ columns: sectionSpec.columns ?? 1,
129
+ fields,
130
+ };
131
+ });
132
+
133
+ const id = (values["id"] as string | undefined) ?? null;
134
+
135
+ return {
136
+ screenId: screen.id,
137
+ entityName: screen.entity,
138
+ id,
139
+ sections,
140
+ ...(screen.slots && { slots: screen.slots }),
141
+ };
142
+ }
143
+
144
+ // Resolves a FieldCondition (undefined | predicate) into its boolean
145
+ // value against the current row values + ctx. `undefined` means "not
146
+ // declared" — caller substitutes the entity-level default.
147
+ //
148
+ // TValues / TCtx mirror the generics on FieldCondition. EditFieldSpec
149
+ // stores predicates as FieldCondition<unknown, unknown>, which is
150
+ // assignable to FieldCondition<TValues, TCtx> for any TValues/TCtx by
151
+ // parameter contravariance (a predicate that accepts `unknown` accepts
152
+ // anything). The cast on ctx is the one rough edge: input.ctx is
153
+ // optional, so at runtime we may pass undefined to a predicate that
154
+ // declared a concrete TCtx — the caller is responsible for not doing
155
+ // that when they narrow TCtx away from `unknown`.
156
+ function evalCondition<TValues, TCtx>(
157
+ condition: FieldCondition<TValues, TCtx> | undefined,
158
+ fallback: boolean,
159
+ values: TValues,
160
+ ctx: TCtx | undefined,
161
+ ): boolean {
162
+ if (condition === undefined) return fallback;
163
+ return condition(values, ctx as TCtx);
164
+ }
@@ -0,0 +1,19 @@
1
+ export type { ComputeEditViewModelInput } from "./edit";
2
+ export { computeEditViewModel } from "./edit";
3
+ export type { ComputeListViewModelInput } from "./list";
4
+ export { computeListViewModel, fieldLabelKey } from "./list";
5
+ export type {
6
+ EditFieldSpec,
7
+ EditFieldViewModel,
8
+ EditSectionSpec,
9
+ EditSectionViewModel,
10
+ EditViewModel,
11
+ FieldConditionCtx,
12
+ FieldRenderer,
13
+ ListColumnSpec,
14
+ ListColumnViewModel,
15
+ ListRowViewModel,
16
+ ListViewModel,
17
+ ScreenSlots,
18
+ Translate,
19
+ } from "./types";
@@ -0,0 +1,158 @@
1
+ import type {
2
+ EntityDefinition,
3
+ EntityListScreenDefinition,
4
+ FieldDefinition,
5
+ } from "@cosmicdrift/kumiko-framework/ui-types";
6
+ import { normalizeListColumn, parseRefTarget } from "@cosmicdrift/kumiko-framework/ui-types";
7
+ import type { ListColumnViewModel, ListRowViewModel, ListViewModel, Translate } from "./types";
8
+
9
+ export type ComputeListViewModelInput = {
10
+ readonly screen: EntityListScreenDefinition;
11
+ readonly entity: EntityDefinition;
12
+ readonly rows: readonly Readonly<Record<string, unknown>>[];
13
+ // Translate callback — normally LocaleResolver.translate. Labels use the
14
+ // i18n convention "{feature}:entity:{entityName}:field:{fieldName}"; if
15
+ // the key is absent from the active bundle, i18next falls back to the
16
+ // key itself, which is fine for dev.
17
+ readonly translate: Translate;
18
+ // Feature + entity name are required for i18n key composition —
19
+ // ScreenDefinition carries `entity: string` but not the feature scope,
20
+ // and i18n keys are prefixed by feature. Passed in by the caller (the
21
+ // renderer knows its host-feature context).
22
+ readonly featureName: string;
23
+ };
24
+
25
+ // Pure transform: takes the declared screen + entity + incoming rows, spits
26
+ // out the flat shape the renderer draws. No conditions, no access-checks —
27
+ // those are list-level decisions that the caller makes BEFORE calling
28
+ // here (e.g. filtering rows by ownership on the server / before query).
29
+ // Field-level read-access (Level 3 of UI-architecture.md §Permission) is a
30
+ // follow-up; this v1 assumes the caller passed a row-filter that drops any
31
+ // field the user may not see.
32
+ export function computeListViewModel(input: ComputeListViewModelInput): ListViewModel {
33
+ const { screen, entity, rows, translate, featureName } = input;
34
+
35
+ const columns: ListColumnViewModel[] = [];
36
+ for (const spec of screen.columns) {
37
+ const normalized = normalizeListColumn(spec);
38
+ const fieldDef = entity.fields[normalized.field];
39
+ if (!fieldDef) {
40
+ // Screen references a field that's not on the entity. Fail loud —
41
+ // the boot-validator (r.screen) should catch this, but a stale
42
+ // field-rename would leave the screen referring to a ghost column
43
+ // until ops re-runs boot. We throw so the renderer sees the error
44
+ // instead of drawing an empty column.
45
+ throw new Error(
46
+ `computeListViewModel: screen "${screen.id}" references unknown field "${normalized.field}" on entity "${screen.entity}"`,
47
+ );
48
+ }
49
+ const label = translate(fieldLabelKey(featureName, screen.entity, normalized.field));
50
+ // Tier 2.7e-3 + Cross-Feature: Reference-Field — entity-String
51
+ // kann same-feature ("user") oder cross-feature ("users:user")
52
+ // sein. parseRefTarget gibt (featureName, entityName), der
53
+ // Renderer baut die Lookup-QN als
54
+ // `<refFeature>:query:<refEntity>:list`.
55
+ const refRaw =
56
+ fieldDef.type === "reference"
57
+ ? (fieldDef as unknown as { entity?: string }).entity
58
+ : undefined;
59
+ const refTarget = refRaw !== undefined ? parseRefTarget(refRaw, featureName) : undefined;
60
+ const refEntity = refTarget?.entityName;
61
+ const refFeature = refTarget?.featureName;
62
+ const refLabelField =
63
+ fieldDef.type === "reference"
64
+ ? ((fieldDef as unknown as { labelField?: string }).labelField ?? "id")
65
+ : undefined;
66
+ // Bei select-Feldern: translated Option-Labels einbacken. Convention
67
+ // matcht den Form-Path → eine Translation-Quelle für List + Form.
68
+ // Missing-Key returnt convention-gemäß den Key zurück; Renderer hat
69
+ // dann humanizeSlug-Fallback.
70
+ const optionLabels =
71
+ fieldDef.type === "select"
72
+ ? buildOptionLabels(
73
+ translate,
74
+ featureName,
75
+ screen.entity,
76
+ normalized.field,
77
+ (fieldDef as unknown as { options?: readonly string[] }).options ?? [],
78
+ )
79
+ : undefined;
80
+ const column: ListColumnViewModel = {
81
+ field: normalized.field,
82
+ label,
83
+ type: fieldDef.type,
84
+ sortable: fieldIsSortable(fieldDef),
85
+ ...(normalized.renderer !== undefined && { renderer: normalized.renderer }),
86
+ ...(optionLabels !== undefined && { optionLabels }),
87
+ ...(refEntity !== undefined && { refEntity }),
88
+ ...(refFeature !== undefined && { refFeature }),
89
+ ...(refLabelField !== undefined && { refLabelField }),
90
+ };
91
+ columns.push(column);
92
+ }
93
+
94
+ const listRows: ListRowViewModel[] = rows.map((row) => ({
95
+ id: String(row["id"] ?? ""),
96
+ values: row,
97
+ }));
98
+
99
+ return {
100
+ screenId: screen.id,
101
+ entityName: screen.entity,
102
+ columns,
103
+ rows: listRows,
104
+ ...(screen.slots && { slots: screen.slots }),
105
+ isEmpty: listRows.length === 0,
106
+ };
107
+ }
108
+
109
+ // Field-i18n-key convention matches what features register translations
110
+ // under (see packages/framework/src/i18n/ for the pattern). Duplicated
111
+ // here as a plain function — the ui-core boundary forbids depending on
112
+ // the i18n module directly.
113
+ export function fieldLabelKey(featureName: string, entityName: string, fieldName: string): string {
114
+ return `${featureName}:entity:${entityName}:field:${fieldName}`;
115
+ }
116
+
117
+ export function fieldOptionLabelKey(
118
+ featureName: string,
119
+ entityName: string,
120
+ fieldName: string,
121
+ value: string,
122
+ ): string {
123
+ return `${featureName}:entity:${entityName}:field:${fieldName}:option:${value}`;
124
+ }
125
+
126
+ // Build a value→label map for a select-field's options. Convention:
127
+ // translate() returns the input key when the lookup misses (i18next
128
+ // default + LocaleResolver convention) — we surface the *raw value* in
129
+ // that case so the renderer's humanizeSlug fallback can take over.
130
+ // Without that fallback, an unlabeled option would render as the full
131
+ // `feature:entity:field:option:value`-key.
132
+ //
133
+ // Shared between list-VM and edit-VM so both builders produce
134
+ // identical option-translations.
135
+ export function buildOptionLabels(
136
+ translate: (key: string, params?: Readonly<Record<string, unknown>>) => string,
137
+ featureName: string,
138
+ entityName: string,
139
+ fieldName: string,
140
+ options: readonly string[],
141
+ ): Readonly<Record<string, string>> {
142
+ const out: Record<string, string> = {};
143
+ for (const value of options) {
144
+ const key = fieldOptionLabelKey(featureName, entityName, fieldName, value);
145
+ const translated = translate(key);
146
+ out[value] = translated === key ? value : translated;
147
+ }
148
+ return out;
149
+ }
150
+
151
+ // A field can declare `sortable: true` on the FieldDefinition. This is
152
+ // framework-level metadata used both by the server's list-query builder
153
+ // (ORDER BY safety) and by the UI (column header click indicator). All
154
+ // field types can bear the flag; it's off by default.
155
+ function fieldIsSortable(field: FieldDefinition): boolean {
156
+ const flag = (field as unknown as { sortable?: boolean }).sortable;
157
+ return flag === true;
158
+ }
@@ -0,0 +1,150 @@
1
+ import type {
2
+ EditFieldSpec,
3
+ EditSectionSpec,
4
+ FieldRenderer,
5
+ ListColumnSpec,
6
+ ScreenSlots,
7
+ } from "@cosmicdrift/kumiko-framework/ui-types";
8
+
9
+ // View-Models — plain data structures produced by computeListViewModel and
10
+ // computeEditViewModel. They flatten the combined [screen-def + entity-def
11
+ // + row-data + user] inputs into a shape the renderer draws directly,
12
+ // without re-resolving conditions or re-reading the entity field map.
13
+ //
14
+ // The renderer's render-list.tsx / render-edit.tsx are essentially
15
+ // (viewModel) => JSX // on web
16
+ // (viewModel) => <View/Text> // on native
17
+ // so everything the renderer needs to render one frame must be in here.
18
+ //
19
+ // Why pre-compute instead of resolving at render-time: predicates
20
+ // (visible/readonly/required) are arbitrary JS closures — running them
21
+ // inside React's render is wasteful (happens on every re-render, even
22
+ // when values haven't changed) and mixes side-effect-free view code
23
+ // with business logic. Computing once on form-state-change gives the
24
+ // renderer pure data and keeps render paths trivial.
25
+
26
+ // --- list view model ---
27
+
28
+ // One column, fully resolved. `label` is the localized string the
29
+ // renderer puts in the column header; view-model builder runs it through
30
+ // LocaleResolver.translate() from the i18nKey wired onto the field.
31
+ // `renderer` passes through ScreenDefinition's FieldRenderer verbatim.
32
+ export type ListColumnViewModel = {
33
+ readonly field: string;
34
+ readonly label: string;
35
+ readonly type: string; // field-type ("text", "number", "money", ...)
36
+ readonly renderer?: FieldRenderer;
37
+ readonly sortable: boolean;
38
+ /** Nur bei `type: "select"` — translated Option-Labels keyed nach raw
39
+ * value. Renderer rendert `optionLabels[value]` statt humanizeSlug
40
+ * wenn vorhanden. Convention-Key: `<feature>:entity:<entity>:field:<field>:option:<value>`. */
41
+ readonly optionLabels?: Readonly<Record<string, string>>;
42
+ /** Nur bei `type: "reference"` — referenced Entity-Name für Bulk-
43
+ * Lookup im Renderer (`<refFeature>:query:<refEntity>:list`). */
44
+ readonly refEntity?: string;
45
+ /** Nur bei `type: "reference"` — Feature-Name in dem die referenced
46
+ * Entity wohnt. Default = current feature. Cross-Feature über
47
+ * qualifizierte Form ("feature:entity") am ReferenceFieldDef. */
48
+ readonly refFeature?: string;
49
+ /** Nur bei `type: "reference"` — Welches Feld der referenced Entity
50
+ * als Display-Wert in der Cell erscheint (Default "id"). */
51
+ readonly refLabelField?: string;
52
+ };
53
+
54
+ export type ListRowViewModel = {
55
+ readonly id: string;
56
+ readonly values: Readonly<Record<string, unknown>>;
57
+ };
58
+
59
+ export type ListViewModel = {
60
+ readonly screenId: string;
61
+ readonly entityName: string;
62
+ readonly columns: readonly ListColumnViewModel[];
63
+ readonly rows: readonly ListRowViewModel[];
64
+ readonly slots?: ScreenSlots;
65
+ // Flags for the renderer to decide what kind of container to draw.
66
+ readonly isEmpty: boolean;
67
+ };
68
+
69
+ // --- edit view model ---
70
+
71
+ // Resolved field — all predicates evaluated, labels translated. The
72
+ // renderer reads `{ visible, readonly, required }` directly without
73
+ // re-running any predicate.
74
+ export type EditFieldViewModel = {
75
+ readonly field: string;
76
+ readonly label: string;
77
+ readonly type: string;
78
+ readonly value: unknown;
79
+ readonly visible: boolean;
80
+ // `readOnly` (camelCase) matches the spec-side name on EditFieldSpec —
81
+ // one convention through the stack. The TS property modifier `readonly`
82
+ // (lowercase) only collides as a key name in type declarations of the
83
+ // spec; here in the view-model we could have used either, but symmetric
84
+ // naming beats clever ergonomics. Parallel-agent chose `readOnly` on
85
+ // the input; we honour it on the output.
86
+ readonly readOnly: boolean;
87
+ readonly required: boolean;
88
+ readonly span?: number;
89
+ readonly renderer?: FieldRenderer;
90
+ /** Nur bei `type: "select"` gesetzt — die zugelassenen Werte. Wird
91
+ * vom Renderer als Dropdown-Optionen genutzt. Quelle ist
92
+ * SelectFieldDef.options aus der EntityDefinition. */
93
+ readonly options?: readonly string[];
94
+ /** Nur bei `type: "select"` gesetzt — translated Labels pro Option,
95
+ * keyed nach raw value. Renderer zeigt `optionLabels[value]` als
96
+ * Dropdown-Label statt raw value. Convention-Key:
97
+ * `<feature>:entity:<entity>:field:<field>:option:<value>`. */
98
+ readonly optionLabels?: Readonly<Record<string, string>>;
99
+ /** Nur bei `type: "text"` gesetzt wenn TextFieldDef.multiline true
100
+ * ist — dann rendert der Renderer textarea statt single-line input.
101
+ * `true` = Default-Zeilen, `{ rows }` = explizite Höhe. */
102
+ readonly multiline?: boolean | { readonly rows?: number };
103
+ /** Nur bei `type: "reference"` gesetzt — Tier 2.7e-3.
104
+ * Die referenced Entity (kurz, ohne feature-prefix). Der Renderer
105
+ * baut die Query-QN als `<refFeature>:query:<refEntity>:list`. */
106
+ readonly refEntity?: string;
107
+ /** Nur bei `type: "reference"` gesetzt — Feature-Name in dem die
108
+ * referenced Entity wohnt. Same-feature default = aktuelles
109
+ * Feature. Cross-Feature wird über "feature:entity" am
110
+ * ReferenceFieldDef.entity erkannt. */
111
+ readonly refFeature?: string;
112
+ /** Nur bei `type: "reference"` gesetzt — Welches Feld der referenced
113
+ * Entity als Display-Label im Dropdown erscheint. Default: "id". */
114
+ readonly refLabelField?: string;
115
+ /** Nur bei `type: "reference"` — Multi-Mode (Tier 2.7e-Multi):
116
+ * Wert ist UUID-Array, Renderer mountet Multi-Combobox mit Tags. */
117
+ readonly refMultiple?: boolean;
118
+ };
119
+
120
+ export type EditSectionViewModel = {
121
+ readonly title: string;
122
+ readonly columns: number;
123
+ readonly fields: readonly EditFieldViewModel[];
124
+ };
125
+
126
+ export type EditViewModel = {
127
+ readonly screenId: string;
128
+ readonly entityName: string;
129
+ readonly id: string | null; // null on create (no existing row)
130
+ readonly sections: readonly EditSectionViewModel[];
131
+ readonly slots?: ScreenSlots;
132
+ };
133
+
134
+ // --- resolver interfaces (host-injected) ---
135
+
136
+ // The view-model builder calls this to translate i18n keys into strings.
137
+ // Normally the host passes the renderer's LocaleResolver.translate
138
+ // directly — keeping the interface narrow here avoids dragging the full
139
+ // LocaleResolver (with subscribe) into a pure compute path.
140
+ export type Translate = (key: string, params?: Readonly<Record<string, unknown>>) => string;
141
+
142
+ // Optional condition context forwarded to field visibility/readonly/required
143
+ // predicates. Same ctx the form-controller uses in conditional-fields — so
144
+ // a predicate that gated "admin-only" there keeps working here.
145
+ export type FieldConditionCtx = unknown;
146
+
147
+ // Re-export the ScreenDef spec halves we don't want ui-core callers to
148
+ // import from @cosmicdrift/kumiko-framework separately. Renderer packages expect a
149
+ // single import surface.
150
+ export type { EditFieldSpec, EditSectionSpec, FieldRenderer, ListColumnSpec, ScreenSlots };