@vectoriox/iox-ui 5.83.0 → 5.84.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.
@@ -231,6 +231,40 @@ export type StockPlacement<T extends StockTarget = StockTarget> = T & { photo: S
231
231
  export interface StockPlanOptions {
232
232
  /** The business type's imagery line (harness-core `BusinessType.imagery`) — its words count for every slot. */
233
233
  imagery?: string;
234
+ /**
235
+ * M7 (premium sites PS4): the SITE the photos are chosen for (its organization). Among the photos that answer a slot
236
+ * nearly as well as the best (`STOCK_SEED_MARGIN`), the seed decides — so two sites of the same kind of business stop
237
+ * opening on the same hero, and the same site always gets the same photos. No seed: the library's order decides.
238
+ */
239
+ seed?: string;
240
+ /** M7: library keys other sites of this studio already show — chosen only when no photo they do not show fits. */
241
+ avoid?: ReadonlyArray<string>;
242
+ }
243
+
244
+ /** How far below the best score a photo may be and still be the seed's choice (one word of the plan's, about). */
245
+ export const STOCK_SEED_MARGIN = 2;
246
+
247
+ /** A stable 32-bit hash (FNV-1a) — the seed's order of the library, the same in every runtime. */
248
+ function seededRank(seed: string, key: string): number {
249
+ let hash = 0x811c9dc5;
250
+ for (const ch of `${seed}:${key}`) {
251
+ hash ^= ch.codePointAt(0)!;
252
+ hash = Math.imul(hash, 0x01000193) >>> 0;
253
+ }
254
+ return hash;
255
+ }
256
+
257
+ /** The best of `pool` for `slot`: the highest score (ties: the library's order), or — with a seed — the seed's pick among
258
+ * those within `STOCK_SEED_MARGIN` of it. */
259
+ function bestFor(pool: ReadonlyArray<StockPhoto>, slot: StockNeed, imagery: ReadonlyArray<string>, seed: string | undefined): StockPhoto | undefined {
260
+ const scored = pool.map(photo => ({ photo, score: scoreStockPhoto(photo, slot, imagery) }));
261
+ const top = scored.reduce((m, s) => Math.max(m, s.score), -Infinity);
262
+ if (!seed) return scored.find(s => s.score === top)?.photo;
263
+ const near = scored.filter(s => s.score >= top - STOCK_SEED_MARGIN);
264
+ return near.reduce<{ photo: StockPhoto; rank: number } | undefined>((best, s) => {
265
+ const rank = seededRank(seed, s.photo.key);
266
+ return !best || rank < best.rank ? { photo: s.photo, rank } : best;
267
+ }, undefined)?.photo;
234
268
  }
235
269
 
236
270
  const intentRank = (intent: string) => STOCK_INTENT_ORDER.indexOf(intent as StockIntent);
@@ -239,15 +273,17 @@ const intentRank = (intent: string) => STOCK_INTENT_ORDER.indexOf(intent as Stoc
239
273
  * Which photo goes where (I2.1). Slots choose in intent order (the hero first, list items last; page order within),
240
274
  * each taking the best-scoring photo that FITS its shape (`stockFits`) and is not already on its page (or among its
241
275
  * type's samples) — a photo no slot has taken yet always before one another page shows. Ties go to the library's
242
- * order. A slot nothing fits keeps its empty box (a sample its abstract picture). None at all when the type says no
243
- * stock, and never a photo the review rejected or flagged. Returned in the slots' order.
276
+ * order — or, with a `seed` (M7), the site's own pick among those nearly as good, and another site's photos (`avoid`) only
277
+ * when nothing fresh fits. A slot nothing fits keeps its empty box (a sample its abstract picture). None at all when the
278
+ * type says no stock, and never a photo the review rejected or flagged. Returned in the slots' order.
244
279
  */
245
280
  export function planStockPhotos<T extends StockTarget>(slots: ReadonlyArray<T>, photos: ReadonlyArray<StockPhoto>, stockAllowed: boolean, opts: StockPlanOptions = {}): Array<StockPlacement<T>> {
246
281
  if (!stockAllowed) return [];
247
282
  const usable = photos.filter(stockPhotoUsable);
248
283
  const imagery = stockWords(opts.imagery);
249
284
  const order = slots.map((slot, i) => ({ slot, i })).sort((a, b) => intentRank(a.slot.intent) - intentRank(b.slot.intent) || a.i - b.i);
250
- const used = new Set<string>();
285
+ // Another site's photos count as already shown: a fresh one that fits always comes first.
286
+ const used = new Set<string>(opts.avoid ?? []);
251
287
  const shownIn = new Map<string, Set<string>>();
252
288
  const placed = new Map<number, StockPlacement<T>>();
253
289
  for (const { slot, i } of order) {
@@ -255,13 +291,7 @@ export function planStockPhotos<T extends StockTarget>(slots: ReadonlyArray<T>,
255
291
  const here = shownIn.get(group) ?? new Set<string>();
256
292
  const candidates = usable.filter(p => !here.has(p.key) && stockFits(p, slot));
257
293
  const fresh = candidates.filter(p => !used.has(p.key));
258
- const pool = fresh.length ? fresh : candidates;
259
- let best: StockPhoto | undefined;
260
- let bestScore = -Infinity;
261
- for (const photo of pool) {
262
- const score = scoreStockPhoto(photo, slot, imagery);
263
- if (score > bestScore) { best = photo; bestScore = score; }
264
- }
294
+ const best = bestFor(fresh.length ? fresh : candidates, slot, imagery, opts.seed);
265
295
  if (!best) continue;
266
296
  used.add(best.key);
267
297
  here.add(best.key);
@@ -10,6 +10,7 @@ import { ContentFieldControl, FIELD_TYPE_OF_CONTROL,
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 { artDirectionIssues } from './art-direction';
13
14
  import { patternListGap } from './recipe-fields';
14
15
  import { SPEC_SITE_PLAN_SHAPE } from './site-spec.generated';
15
16
  import {
@@ -76,6 +77,8 @@ export enum SitePlanIssueCode {
76
77
  /** SC6b: the owner's film — pictures that cannot play, too few to play smoothly, or a film's pattern on a plan with
77
78
  * no film (or twice). Set by the build, never by a model. */
78
79
  Film = 'film',
80
+ /** M7: the art direction is not a picture world with its five parts. Set by the generation, never by a model. */
81
+ ArtDirection = 'art-direction',
79
82
  }
80
83
 
81
84
  export type SitePlanIssue = PathIssue<SitePlanIssueCode>;
@@ -115,6 +118,7 @@ const CMS_SOURCE_KEYS = shapeKeys(SHAPE['cmsDataSource']);
115
118
  const RELATIVE_SOURCE_KEYS = shapeKeys(SHAPE['relativeDataSource']);
116
119
  const PAGE_KEYS = shapeKeys(SHAPE['page']);
117
120
  const FILM_KEYS = shapeKeys(SHAPE['film']);
121
+ const ART_DIRECTION_KEYS = shapeKeys(SHAPE['artDirection']);
118
122
  const SECTION_KEYS = shapeKeys(SHAPE['section']);
119
123
  const GLOBAL_POSITION: Readonly<Record<`${GlobalSlot}`, `${PatternGlobalPosition}`>> = {
120
124
  [GlobalSlot.Header]: PatternGlobalPosition.Before,
@@ -508,6 +512,15 @@ function validateFilm(c: Collector, film: unknown, pages: unknown[]): void {
508
512
  });
509
513
  }
510
514
 
515
+ /** M7: what every picture made for the site shares — a known world and its five parts (`artDirectionIssues`). */
516
+ function validateArtDirection(c: Collector, direction: unknown): void {
517
+ if (direction === undefined) return;
518
+ if (isObject(direction)) c.keys(direction, ART_DIRECTION_KEYS, 'artDirection');
519
+ for (const issue of artDirectionIssues(direction)) {
520
+ c.add(issue.path ? SitePlanIssueCode.ArtDirection : SitePlanIssueCode.Shape, issue.path ? `artDirection.${issue.path}` : 'artDirection', issue.message);
521
+ }
522
+ }
523
+
511
524
  function validatePageSeo(c: Collector, seo: unknown, path: string): void {
512
525
  if (seo === undefined) return;
513
526
  if (!isObject(seo)) return c.add(SitePlanIssueCode.Shape, path, 'must be an object');
@@ -556,6 +569,7 @@ export function validateSitePlan(plan: unknown): SitePlanValidation {
556
569
  validateGlobals(c, plan['globals']);
557
570
  validateScenes(c, c.array(plan, 'pages', ''), typeof design['animate'] === 'string' ? design['animate'] : null);
558
571
  validateFilm(c, plan['film'], c.array(plan, 'pages', ''));
572
+ validateArtDirection(c, plan['artDirection']);
559
573
  return { valid: c.issues.length === 0, issues: c.issues };
560
574
  }
561
575
 
@@ -1,5 +1,5 @@
1
1
  import type { DensityId, DesignMotion, MoodId, MotionId, TintId } from '@vectoriox/iox-ui/design-core';
2
- import type { FactKey } from '@vectoriox/iox-ui/generation-core';
2
+ import type { FactKey, PictureWorld } from '@vectoriox/iox-ui/generation-core';
3
3
 
4
4
  import {
5
5
  SPEC_BUSINESS_TYPES, SPEC_HARNESS_SOURCE, SPEC_NEEDS, SPEC_STRUCTURES, SPEC_USER_AVOIDS,
@@ -115,6 +115,8 @@ export interface BusinessType {
115
115
  * so a type capped below it never gets one. Binds generated output only — the owner may choose any level. */
116
116
  readonly ceiling: DesignMotion['animate'];
117
117
  readonly imagery: string;
118
+ /** M7: the picture world a site's made pictures start in (generation-core `artDirectionFor`; contract site.pictureWorlds). */
119
+ readonly world: `${PictureWorld}`;
118
120
  readonly pages: ReadonlyArray<string>;
119
121
  /** The needs its page set implies, in page order (derived from `pages` at contract build). */
120
122
  readonly needs: ReadonlyArray<NeedId>;
@@ -1,6 +1,6 @@
1
1
  import type { DesignChoices } from '@vectoriox/iox-ui/design-core';
2
2
  import type { FrameSequence } from '@vectoriox/iox-ui/media-core';
3
- import type { AiGenerationStage, AiStep, PlanChange, PlanEdit, SitePlan } from '@vectoriox/iox-ui/generation-core';
3
+ import type { AiGenerationStage, AiRefusalCode, AiStep, PictureRefusal, PlanChange, PlanEdit, SitePlan } from '@vectoriox/iox-ui/generation-core';
4
4
 
5
5
  import type { StyleRead } from './rank';
6
6
  import type { SiteAnalysis } from './site-analysis';
@@ -73,6 +73,44 @@ export interface PlanRevisionView {
73
73
  changed?: PlanChange[];
74
74
  }
75
75
 
76
+ /** Why no picture can be made for this site right now (the Plan step says it instead of offering the switch). */
77
+ export enum PicturesUnavailable {
78
+ /** No slot is left after the owner's photos and the refusals. */
79
+ Nothing = 'nothing',
80
+ /** The key in use has no model that makes pictures (a Claude key does not). */
81
+ NoModel = 'no-model',
82
+ }
83
+
84
+ /**
85
+ * M7: the pictures a generation may have MADE, as the Plan step shows them before the owner says yes, and while they are
86
+ * made. `estimate` is the cost shown — every picture and its review plus the second tries (generation-core
87
+ * `estimatePictures`) — and the most the run may spend on them.
88
+ */
89
+ export interface GenerationPicturesView {
90
+ /** Pictures that would be made (after the owner's photos and the refusals). */
91
+ count: number;
92
+ /** The owner's photos that go first. */
93
+ own: number;
94
+ /** Slots no picture is made for, by why. */
95
+ refused: Partial<Record<`${PictureRefusal}`, number>>;
96
+ /** Credits — the cost shown and the run's ceiling. */
97
+ estimate: number;
98
+ /** One picture with its review, in credits. */
99
+ each: number;
100
+ rerolls: number;
101
+ /** The model that makes them, or null. */
102
+ model: string | null;
103
+ /** Why none can be made now — instead of the switch. A refusal code when credits or the plan say no. */
104
+ unavailable?: `${PicturesUnavailable}` | `${AiRefusalCode}`;
105
+ /** The owner said yes (Build with pictures): the run's progress. */
106
+ wanted?: boolean;
107
+ made?: number;
108
+ /** Slots whose review said no to every try — stock or the empty box stands in. */
109
+ failed?: number;
110
+ /** Credits the pictures have cost so far (the ledger). */
111
+ spent?: number;
112
+ }
113
+
76
114
  export interface AiGenerationView {
77
115
  _id: string;
78
116
  organization: string;
@@ -101,6 +139,17 @@ export interface AiGenerationView {
101
139
  /** Derived from the ledger — the running credit total. */
102
140
  spent: number;
103
141
  error?: { code: string; detail: string; providerMessage?: string; issues?: Array<{ code: string; path: string }> };
142
+ /** M7: the pictures that may be made (on the Plan step) or are being made. Absent: no plan yet. */
143
+ pictures?: GenerationPicturesView;
144
+ }
145
+
146
+ /** `POST …/ai/generations/:id/build` (and `/auto`). */
147
+ export interface GenerationBuildRequest {
148
+ replaceGlobals?: boolean;
149
+ /** SC6b: the owner's film as pictures. */
150
+ film?: FrameSequence;
151
+ /** M7: make pictures for the site first (the cost shown on the Plan step). */
152
+ pictures?: boolean;
104
153
  }
105
154
 
106
155
  /** `POST …/ai/generations`. */
@@ -207,6 +207,7 @@ export const SPEC_BUSINESS_TYPES = [
207
207
  "between": "cinematic"
208
208
  },
209
209
  "ceiling": "expressive",
210
+ "world": "documentary",
210
211
  "imagery": "Your own photos only, large and uncropped; never stock.",
211
212
  "pages": [
212
213
  "home",
@@ -312,6 +313,7 @@ export const SPEC_BUSINESS_TYPES = [
312
313
  "between": "cinematic"
313
314
  },
314
315
  "ceiling": "expressive",
316
+ "world": "architectural",
315
317
  "imagery": "Wide, bright room shots; details of materials.",
316
318
  "pages": [
317
319
  "home",
@@ -422,6 +424,7 @@ export const SPEC_BUSINESS_TYPES = [
422
424
  "between": "fade"
423
425
  },
424
426
  "ceiling": "expressive",
427
+ "world": "architectural",
425
428
  "imagery": "Large project photos and drawings.",
426
429
  "pages": [
427
430
  "home",
@@ -531,6 +534,7 @@ export const SPEC_BUSINESS_TYPES = [
531
534
  "between": "slide"
532
535
  },
533
536
  "ceiling": "expressive",
537
+ "world": "hard-light",
534
538
  "imagery": "Case-study images of your own work.",
535
539
  "pages": [
536
540
  "home",
@@ -630,6 +634,7 @@ export const SPEC_BUSINESS_TYPES = [
630
634
  "between": "fade"
631
635
  },
632
636
  "ceiling": "expressive",
637
+ "world": "drawing",
633
638
  "imagery": "Your pieces, photographed flat and true to colour.",
634
639
  "pages": [
635
640
  "home",
@@ -735,6 +740,7 @@ export const SPEC_BUSINESS_TYPES = [
735
740
  "between": "slide"
736
741
  },
737
742
  "ceiling": "expressive",
743
+ "world": "macro",
738
744
  "imagery": "Your pieces and hands at work; natural light.",
739
745
  "pages": [
740
746
  "home",
@@ -833,6 +839,7 @@ export const SPEC_BUSINESS_TYPES = [
833
839
  "between": "cinematic"
834
840
  },
835
841
  "ceiling": "expressive",
842
+ "world": "macro",
836
843
  "imagery": "Close-ups on a plain ground.",
837
844
  "pages": [
838
845
  "home",
@@ -935,6 +942,7 @@ export const SPEC_BUSINESS_TYPES = [
935
942
  "between": "slide"
936
943
  },
937
944
  "ceiling": "expressive",
945
+ "world": "low-key",
938
946
  "imagery": "Healed work, clean photos.",
939
947
  "pages": [
940
948
  "home",
@@ -1031,6 +1039,7 @@ export const SPEC_BUSINESS_TYPES = [
1031
1039
  "between": "fade"
1032
1040
  },
1033
1041
  "ceiling": "standard",
1042
+ "world": "high-key",
1034
1043
  "imagery": "Rooms, textures, hands; no faces needed.",
1035
1044
  "pages": [
1036
1045
  "home",
@@ -1150,6 +1159,7 @@ export const SPEC_BUSINESS_TYPES = [
1150
1159
  "between": "fade"
1151
1160
  },
1152
1161
  "ceiling": "lively",
1162
+ "world": "high-key",
1153
1163
  "imagery": "Your own before-and-after photos.",
1154
1164
  "pages": [
1155
1165
  "home",
@@ -1252,6 +1262,7 @@ export const SPEC_BUSINESS_TYPES = [
1252
1262
  "between": "fade"
1253
1263
  },
1254
1264
  "ceiling": "lively",
1265
+ "world": "high-key",
1255
1266
  "imagery": "The room, real classes, daylight.",
1256
1267
  "pages": [
1257
1268
  "home",
@@ -1362,6 +1373,7 @@ export const SPEC_BUSINESS_TYPES = [
1362
1373
  "between": "slide"
1363
1374
  },
1364
1375
  "ceiling": "lively",
1376
+ "world": "hard-light",
1365
1377
  "imagery": "Real people training; movement.",
1366
1378
  "pages": [
1367
1379
  "home",
@@ -1464,6 +1476,7 @@ export const SPEC_BUSINESS_TYPES = [
1464
1476
  "between": "none"
1465
1477
  },
1466
1478
  "ceiling": "subtle",
1479
+ "world": "documentary",
1467
1480
  "imagery": "A calm room, daylight; your portrait.",
1468
1481
  "pages": [
1469
1482
  "home",
@@ -1574,6 +1587,7 @@ export const SPEC_BUSINESS_TYPES = [
1574
1587
  "between": "fade"
1575
1588
  },
1576
1589
  "ceiling": "subtle",
1590
+ "world": "high-key",
1577
1591
  "imagery": "Your team and rooms; no stock doctors.",
1578
1592
  "pages": [
1579
1593
  "home",
@@ -1679,6 +1693,7 @@ export const SPEC_BUSINESS_TYPES = [
1679
1693
  "between": "fade"
1680
1694
  },
1681
1695
  "ceiling": "subtle",
1696
+ "world": "high-key",
1682
1697
  "imagery": "Your clinic and team.",
1683
1698
  "pages": [
1684
1699
  "home",
@@ -1785,6 +1800,7 @@ export const SPEC_BUSINESS_TYPES = [
1785
1800
  "between": "fade"
1786
1801
  },
1787
1802
  "ceiling": "standard",
1803
+ "world": "documentary",
1788
1804
  "imagery": "Real animals you treat (with permission).",
1789
1805
  "pages": [
1790
1806
  "home",
@@ -1883,6 +1899,7 @@ export const SPEC_BUSINESS_TYPES = [
1883
1899
  "between": "fade"
1884
1900
  },
1885
1901
  "ceiling": "expressive",
1902
+ "world": "low-key",
1886
1903
  "imagery": "Your own food and room photos.",
1887
1904
  "pages": [
1888
1905
  "home",
@@ -1992,6 +2009,7 @@ export const SPEC_BUSINESS_TYPES = [
1992
2009
  "between": "slide"
1993
2010
  },
1994
2011
  "ceiling": "lively",
2012
+ "world": "documentary",
1995
2013
  "imagery": "Your bakes and drinks, close up.",
1996
2014
  "pages": [
1997
2015
  "home",
@@ -2095,6 +2113,7 @@ export const SPEC_BUSINESS_TYPES = [
2095
2113
  "between": "cinematic"
2096
2114
  },
2097
2115
  "ceiling": "expressive",
2116
+ "world": "nocturne",
2098
2117
  "imagery": "The cellar, the bar, the bottle.",
2099
2118
  "pages": [
2100
2119
  "home",
@@ -2198,6 +2217,7 @@ export const SPEC_BUSINESS_TYPES = [
2198
2217
  "between": "slide"
2199
2218
  },
2200
2219
  "ceiling": "expressive",
2220
+ "world": "high-key",
2201
2221
  "imagery": "Product photos on plain grounds; real people wearing them.",
2202
2222
  "pages": [
2203
2223
  "home",
@@ -2293,6 +2313,7 @@ export const SPEC_BUSINESS_TYPES = [
2293
2313
  "between": "fade"
2294
2314
  },
2295
2315
  "ceiling": "lively",
2316
+ "world": "macro",
2296
2317
  "imagery": "Your own arrangements in daylight.",
2297
2318
  "pages": [
2298
2319
  "home",
@@ -2389,6 +2410,7 @@ export const SPEC_BUSINESS_TYPES = [
2389
2410
  "between": "fade"
2390
2411
  },
2391
2412
  "ceiling": "expressive",
2413
+ "world": "documentary",
2392
2414
  "imagery": "Pieces in real rooms and on plain grounds.",
2393
2415
  "pages": [
2394
2416
  "home",
@@ -2486,6 +2508,7 @@ export const SPEC_BUSINESS_TYPES = [
2486
2508
  "between": "fade"
2487
2509
  },
2488
2510
  "ceiling": "standard",
2511
+ "world": "architectural",
2489
2512
  "imagery": "Your portrait; diagrams over stock.",
2490
2513
  "pages": [
2491
2514
  "home",
@@ -2587,6 +2610,7 @@ export const SPEC_BUSINESS_TYPES = [
2587
2610
  "between": "fade"
2588
2611
  },
2589
2612
  "ceiling": "standard",
2613
+ "world": "documentary",
2590
2614
  "imagery": "Your portrait, working with people.",
2591
2615
  "pages": [
2592
2616
  "home",
@@ -2683,6 +2707,7 @@ export const SPEC_BUSINESS_TYPES = [
2683
2707
  "between": "none"
2684
2708
  },
2685
2709
  "ceiling": "subtle",
2710
+ "world": "architectural",
2686
2711
  "imagery": "Your team; your office.",
2687
2712
  "pages": [
2688
2713
  "home",
@@ -2793,6 +2818,7 @@ export const SPEC_BUSINESS_TYPES = [
2793
2818
  "between": "slide"
2794
2819
  },
2795
2820
  "ceiling": "lively",
2821
+ "world": "architectural",
2796
2822
  "imagery": "Your own property photos; never stock.",
2797
2823
  "pages": [
2798
2824
  "home",
@@ -2898,6 +2924,7 @@ export const SPEC_BUSINESS_TYPES = [
2898
2924
  "between": "fade"
2899
2925
  },
2900
2926
  "ceiling": "standard",
2927
+ "world": "documentary",
2901
2928
  "imagery": "Real people you help (with permission).",
2902
2929
  "pages": [
2903
2930
  "home",
@@ -2991,6 +3018,7 @@ export const SPEC_BUSINESS_TYPES = [
2991
3018
  "between": "cinematic"
2992
3019
  },
2993
3020
  "ceiling": "expressive",
3021
+ "world": "nocturne",
2994
3022
  "imagery": "Real events at your venue.",
2995
3023
  "pages": [
2996
3024
  "home",
@@ -3093,6 +3121,7 @@ export const SPEC_BUSINESS_TYPES = [
3093
3121
  "between": "cinematic"
3094
3122
  },
3095
3123
  "ceiling": "expressive",
3124
+ "world": "documentary",
3096
3125
  "imagery": "Rooms, views, breakfast; your own photos.",
3097
3126
  "pages": [
3098
3127
  "home",
@@ -3201,6 +3230,7 @@ export const SPEC_BUSINESS_TYPES = [
3201
3230
  "between": "slide"
3202
3231
  },
3203
3232
  "ceiling": "lively",
3233
+ "world": "documentary",
3204
3234
  "imagery": "Real classes (with permission).",
3205
3235
  "pages": [
3206
3236
  "home",
@@ -112,12 +112,15 @@
112
112
 
113
113
  /* ── Date (premium N8): the field is a BUTTON saying the date in words; the browser's date box is never shown (it
114
114
  cannot take the look — its icon was black on a dark flavor — and on an iPhone it is an empty box until tapped). ── */
115
+ /* As wide as a text box beside it (the browser's own ~20 characters), wider for longer words — never the whole row
116
+ while its neighbours are not; an author's part width still wins (the node's rule out-specifies this one). */
115
117
  .iox-field__control--date {
116
- display: flex;
118
+ display: inline-flex;
117
119
  align-items: center;
118
120
  justify-content: space-between;
119
121
  gap: 10px;
120
- inline-size: 100%;
122
+ min-inline-size: 15em;
123
+ max-inline-size: 100%;
121
124
  text-align: start;
122
125
  cursor: pointer;
123
126
  -webkit-tap-highlight-color: transparent;
@@ -237,6 +240,10 @@ dialog.iox-cal::backdrop { background: transparent; }
237
240
  @starting-style { dialog.iox-cal[open] { opacity: 0; transform: none; } }
238
241
  dialog.iox-cal[data-iox-closing] { opacity: 0; transition-duration: 120ms; }
239
242
  }
243
+ /* Opened from the KEYBOARD (Enter / Space on the date button): it simply appears, and Esc or Enter on a day closes it
244
+ at once — keyboard-initiated actions never animate (Emil 2). The pointer gets the motion above. */
245
+ dialog.iox-cal[data-iox-instant],
246
+ dialog.iox-cal[data-iox-instant]::backdrop { transition: none !important; }
240
247
 
241
248
  /* The submit button follows its FORM's state (vectoriox/iox-ui#42, premium N2). Its three labels (forms-core
242
249
  submitButtonLabelsHtml / IoxSubmitButtonComponent) share one grid cell, so the button keeps the longest one's width
@@ -266,6 +273,8 @@ dialog.iox-cal::backdrop { background: transparent; }
266
273
  transition: opacity 160ms cubic-bezier(0.23, 1, 0.32, 1), filter 160ms cubic-bezier(0.23, 1, 0.32, 1), visibility 0s;
267
274
  }
268
275
  .iox-submit[aria-busy="true"] { cursor: progress; }
276
+ /* Sent is final: the button stays as the confirmation, and no longer says it can be pressed. */
277
+ .iox-form[data-iox-state="success"] .iox-submit { cursor: default; }
269
278
  /* Sent draws its check ONCE (the rare-tier beat — Emil 135/136: stroke-dashoffset over 400ms on the check-draw
270
279
  curve), a beat after the label arrives. Leaving Sent resets it at once, so the next success draws it again. */
271
280
  .iox-submit__check { flex: none; }