@vectoriox/iox-ui 5.63.0 → 5.65.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.
@@ -10,6 +10,7 @@ import {
10
10
  FACT_KEYS, GlobalSlot, PAGE_ARCHETYPES, SITE_NEED_IDS, PlanDataSourceMode, PlanDataSourceType, RelativeOp, REQUIRED_CONTENT_ROLES, SitePlan, SitePlanSourceKind,
11
11
  SITE_LOCALES, SITE_PLAN_VERSION,
12
12
  } from './site-plan.model';
13
+ import { patternListGap } from './recipe-fields';
13
14
  import { SPEC_SITE_PLAN_SHAPE } from './site-spec.generated';
14
15
  import {
15
16
  isNonEmptyString, isPlainObject as isObject, PathIssue, PathIssueCollector, pathAt as at, PlainObject as Obj, requiredShapeKeys, ShapeEntry, shapeKeys,
@@ -187,7 +188,12 @@ function validateDesign(c: Collector, design: unknown): void {
187
188
  if (design['fonts'] !== undefined && !isObject(design['fonts'])) bad('fonts', 'must be an object');
188
189
  }
189
190
 
190
- interface ContentTypeInfo { fields: Set<string>; roles: Record<string, string>; }
191
+ interface ContentTypeInfo {
192
+ fields: Set<string>;
193
+ /** The fields as a recipe's pickers read them (`recipe-fields`). */
194
+ described: Array<{ name: string; type?: string; controller?: string }>;
195
+ roles: Record<string, string>;
196
+ }
191
197
 
192
198
  function validateContentTypes(c: Collector, list: unknown[]): Map<string, ContentTypeInfo> {
193
199
  const types = new Map<string, ContentTypeInfo>();
@@ -223,11 +229,13 @@ function validateContentTypes(c: Collector, list: unknown[]): Map<string, Conten
223
229
  }
224
230
  }
225
231
  if (name && types.has(name)) c.add(SitePlanIssueCode.Duplicate, `${path}.name`, `content type "${name}" is used twice`);
226
- else if (name) types.set(name, { fields: fieldSet, roles });
232
+ else if (name) types.set(name, { fields: fieldSet, described: fields.filter(isObject).map(describeField), roles });
227
233
  });
228
234
  return types;
229
235
  }
230
236
 
237
+ const describeField = (f: Obj) => ({ name: String(f['name']), type: typeof f['type'] === 'string' ? f['type'] : undefined, controller: typeof f['controller'] === 'string' ? f['controller'] : undefined });
238
+
231
239
  /** A content type's roles (L0-3): known roles only, each played by a field of the type; `title` always; `gallery` a list. */
232
240
  function validateRoles(c: Collector, roles: unknown, path: string, typeName: string | null, fields: Set<string>, fieldTypes: Map<unknown, unknown>): Record<string, string> {
233
241
  if (!isObject(roles)) { c.add(SitePlanIssueCode.Shape, path, 'must be an object (which field plays each role)'); return {}; }
@@ -396,6 +404,11 @@ function validatePages(c: Collector, list: unknown[], types: Map<string, Content
396
404
  const src = sources.get(binding);
397
405
  if (!src) c.add(SitePlanIssueCode.UnknownDataSource, `${sp}.binding`, `no data source "${binding}"`);
398
406
  else if (src.type !== PlanDataSourceType.Cms || src.mode !== PlanDataSourceMode.Query) c.add(SitePlanIssueCode.Binding, `${sp}.binding`, `"${binding}" is not a list source`);
407
+ else {
408
+ const listed = src.contentType ? types.get(src.contentType) : undefined;
409
+ const gap = listed ? patternListGap(pattern, listed.described) : null;
410
+ if (gap) c.add(SitePlanIssueCode.Binding, `${sp}.binding`, `"${binding}" lists ${src.contentType}, which has no ${gap} — bind "${patternId}" to a list that has${patternNeedsBinding(pattern) ? '' : ', or to none'}`);
411
+ }
399
412
  }
400
413
  } else if (binding) c.add(SitePlanIssueCode.Binding, `${sp}.binding`, `"${patternId}" repeats nothing — it takes no binding`);
401
414
 
@@ -118,15 +118,17 @@ iox-gallery.iox-es-media[data-iox-empty] > .iox-gallery {
118
118
 
119
119
  /* The DRAWER (board 1, board-8 Q9): a Dialog wearing an element style, placed at the start or end edge, is
120
120
  a full-height sheet on that edge — 100dvh (never 100vh: Safari's toolbar), no gap to the edge, the
121
- radius kept on its LEADING edge only (a placement rule, not a token). It slides in over dur-base, which
122
- the reduced-motion token block sets to 0ms: under reduced motion it simply appears. Beats the engine's
123
- placement margins (0,2,1) and the node rule; a Dialog saved before element styles keeps its look. */
121
+ radius kept on its LEADING edge only (a placement rule, not a token). Beats the engine's placement
122
+ margins (0,2,1) and the node rule; a Dialog saved before element styles keeps its look. */
124
123
  dialog[data-iox-dialog].iox-es:is([data-iox-dialog-placement='end'], [data-iox-dialog-placement='start']) {
125
124
  margin-block: 0;
126
125
  height: 100dvh;
127
126
  max-height: 100dvh;
128
- transition: translate var(--iox-dur-base) var(--iox-ease-standard), overlay var(--iox-dur-base) allow-discrete, display var(--iox-dur-base) allow-discrete;
129
127
  }
128
+ /* A NAVIGATION's drawer is padded like the header it opens from, so its Close (the menu button's size,
129
+ generation-core nav-shape) lands where the menu button is — the same button, turned into an X. The token is
130
+ re-declared, not the padding overridden: a padding the author sets on the drawer still wins. */
131
+ .iox-es.iox-role-nav-drawer { --iox-dialog-pad: var(--iox-nav-pad); }
130
132
  dialog[data-iox-dialog].iox-es[data-iox-dialog-placement='end'] {
131
133
  margin-inline: auto 0;
132
134
  border-start-end-radius: 0;
@@ -137,11 +139,68 @@ dialog[data-iox-dialog].iox-es[data-iox-dialog-placement='start'] {
137
139
  border-start-start-radius: 0;
138
140
  border-end-start-radius: 0;
139
141
  }
142
+ /* How a styled dialog COMES and GOES (CC1 follow-up). Three states, one look each:
143
+ · at rest, open — in place, the scrim on;
144
+ · entering (`@starting-style`) and leaving (`data-iox-closing`, set by actions-core `closeDialogAnimated`
145
+ for the whole exit, and `:not([open])` for a browser that closes it natively) — the drawer off its edge,
146
+ any other placement faded 12px toward where it sits, the scrim clear.
147
+ Entering decelerates (ease-enter), leaving accelerates (ease-exit), both over dur-base — the look's motion
148
+ tokens, which the reduced-motion token block collapses to 0ms: under reduced motion it simply appears and
149
+ goes. `overlay`/`display` allow-discrete keep a natively closed dialog in the top layer for its exit. */
150
+ dialog[data-iox-dialog].iox-es {
151
+ --dialog-exit-x: 0px;
152
+ --dialog-exit-y: 12px;
153
+ --dialog-exit-opacity: 0;
154
+ transition:
155
+ translate var(--iox-dur-base) var(--iox-ease-enter),
156
+ opacity var(--iox-dur-base) var(--iox-ease-enter),
157
+ overlay var(--iox-dur-base) allow-discrete,
158
+ display var(--iox-dur-base) allow-discrete;
159
+ }
160
+ dialog[data-iox-dialog].iox-es[data-iox-dialog-placement='top'] { --dialog-exit-y: -12px; }
161
+ dialog[data-iox-dialog].iox-es:is([data-iox-dialog-placement='end'], [data-iox-dialog-placement='start']) {
162
+ --dialog-exit-x: 100%;
163
+ --dialog-exit-y: 0px;
164
+ --dialog-exit-opacity: 1;
165
+ }
166
+ dialog[data-iox-dialog].iox-es[data-iox-dialog-placement='start'],
167
+ [dir='rtl'] dialog[data-iox-dialog].iox-es[data-iox-dialog-placement='end'] { --dialog-exit-x: -100%; }
168
+ [dir='rtl'] dialog[data-iox-dialog].iox-es[data-iox-dialog-placement='start'] { --dialog-exit-x: 100%; }
169
+ dialog[data-iox-dialog].iox-es:is([data-iox-closing], :not([open])) {
170
+ translate: var(--dialog-exit-x) var(--dialog-exit-y);
171
+ opacity: var(--dialog-exit-opacity);
172
+ transition-timing-function: var(--iox-ease-exit);
173
+ }
174
+ @starting-style {
175
+ dialog[data-iox-dialog].iox-es[open] {
176
+ translate: var(--dialog-exit-x) var(--dialog-exit-y);
177
+ opacity: var(--dialog-exit-opacity);
178
+ }
179
+ }
180
+ dialog[data-iox-dialog].iox-es::backdrop {
181
+ transition:
182
+ opacity var(--iox-dur-base) var(--iox-ease-enter),
183
+ overlay var(--iox-dur-base) allow-discrete,
184
+ display var(--iox-dur-base) allow-discrete;
185
+ }
186
+ dialog[data-iox-dialog].iox-es:is([data-iox-closing], :not([open]))::backdrop {
187
+ opacity: 0;
188
+ transition-timing-function: var(--iox-ease-exit);
189
+ }
140
190
  @starting-style {
141
- dialog[data-iox-dialog].iox-es[data-iox-dialog-placement='end'][open] { translate: 100% 0; }
142
- dialog[data-iox-dialog].iox-es[data-iox-dialog-placement='start'][open] { translate: -100% 0; }
143
- [dir='rtl'] dialog[data-iox-dialog].iox-es[data-iox-dialog-placement='end'][open] { translate: -100% 0; }
144
- [dir='rtl'] dialog[data-iox-dialog].iox-es[data-iox-dialog-placement='start'][open] { translate: 100% 0; }
191
+ dialog[data-iox-dialog].iox-es[open]::backdrop { opacity: 0; }
192
+ }
193
+ /* The drawer's pages follow it in, a beat apart (board 1) — a quarter of dur-fast per link, so they collapse
194
+ with the tokens under reduced motion. They leave with the sheet, not one by one. */
195
+ dialog[data-iox-dialog].iox-es[open]:not([data-iox-closing]) .iox-role-nav-drawer-links > * {
196
+ animation: iox-drawer-link-in var(--iox-dur-base) var(--iox-ease-enter) both;
197
+ }
198
+ dialog[data-iox-dialog].iox-es[open] .iox-role-nav-drawer-links > :nth-child(2) { animation-delay: calc(var(--iox-dur-fast) * 0.25); }
199
+ dialog[data-iox-dialog].iox-es[open] .iox-role-nav-drawer-links > :nth-child(3) { animation-delay: calc(var(--iox-dur-fast) * 0.5); }
200
+ dialog[data-iox-dialog].iox-es[open] .iox-role-nav-drawer-links > :nth-child(4) { animation-delay: calc(var(--iox-dur-fast) * 0.75); }
201
+ dialog[data-iox-dialog].iox-es[open] .iox-role-nav-drawer-links > :nth-child(n + 5) { animation-delay: var(--iox-dur-fast); }
202
+ @keyframes iox-drawer-link-in {
203
+ from { opacity: 0; translate: 0 8px; }
145
204
  }
146
205
 
147
206
  /* A navigation's link lists (board 1): no underline at rest — the current page's link is the one