@vectoriox/iox-ui 5.64.0 → 5.66.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.
- package/actions-core/index.d.ts +62 -2
- package/design-core/index.d.ts +7 -1
- package/fesm2022/vectoriox-iox-ui-actions-core.mjs +80 -1
- package/fesm2022/vectoriox-iox-ui-actions-core.mjs.map +1 -1
- package/fesm2022/vectoriox-iox-ui-design-core.mjs +14 -1
- package/fesm2022/vectoriox-iox-ui-design-core.mjs.map +1 -1
- package/fesm2022/vectoriox-iox-ui-generation-core.mjs +31 -8
- package/fesm2022/vectoriox-iox-ui-generation-core.mjs.map +1 -1
- package/fesm2022/vectoriox-iox-ui-harness-core.mjs +19 -2
- package/fesm2022/vectoriox-iox-ui-harness-core.mjs.map +1 -1
- package/harness-core/index.d.ts +18 -2
- package/package.json +1 -1
- package/src/actions-core/dialog-motion.ts +104 -0
- package/src/actions-core/public-api.ts +1 -0
- package/src/design-core/style-presets.ts +13 -0
- package/src/generation-core/nav-shape.ts +31 -11
- package/src/harness-core/rank.ts +30 -1
- package/styles/iox-design.css +67 -8
package/harness-core/index.d.ts
CHANGED
|
@@ -2335,6 +2335,22 @@ declare function rankStylePresets(input: RankInput, presets?: ReadonlyArray<Styl
|
|
|
2335
2335
|
* proper name); a type's or the owner's advice and the shared-moods line are English in the contract yet.
|
|
2336
2336
|
*/
|
|
2337
2337
|
declare function presetReason(ranked: RankedPreset, preset: StylePreset | undefined, locale: string | null | undefined): string;
|
|
2338
|
+
/** One flavor's styles in Design ▸ Look: its presets in ranking order. */
|
|
2339
|
+
interface PresetFlavorGroup {
|
|
2340
|
+
flavorId: string;
|
|
2341
|
+
presets: RankedPreset[];
|
|
2342
|
+
}
|
|
2343
|
+
/**
|
|
2344
|
+
* Design ▸ Look's grid (decided 2026-10-28, Doron: "All 24, grouped by flavor" — reverses Q7 and the wizard's best 6):
|
|
2345
|
+
* every ranked style under its flavor, the flavors in catalogue order (Crisp · Linen · Midnight · Glass · Bloom · Pop),
|
|
2346
|
+
* each flavor's styles best-ranked first. A ranking with no basis (no business, no moods) is catalogue order, so the
|
|
2347
|
+
* groups are too. One rule for both hosts.
|
|
2348
|
+
*/
|
|
2349
|
+
declare function presetsByFlavor(ranked: ReadonlyArray<RankedPreset>, presets?: ReadonlyArray<StylePreset>, flavors?: ReadonlyArray<{
|
|
2350
|
+
id: string;
|
|
2351
|
+
}>): PresetFlavorGroup[];
|
|
2352
|
+
/** The style the ranking puts first — only when it had something to rank by (a positive score); else none is "best". */
|
|
2353
|
+
declare function bestRankedPreset(ranked: ReadonlyArray<RankedPreset>): string | null;
|
|
2338
2354
|
declare enum StyleReadSourceKind {
|
|
2339
2355
|
Image = "image",
|
|
2340
2356
|
Site = "site"
|
|
@@ -2994,5 +3010,5 @@ type SiteAuditView = SiteAudit & {
|
|
|
2994
3010
|
advice: LintFinding[];
|
|
2995
3011
|
};
|
|
2996
3012
|
|
|
2997
|
-
export { ANIMATE_ADVICE_STEPS, AnalysisIssueCode, AnalysisLocale, AvoidCheck, BUSINESS_TYPES, BUSINESS_TYPE_IDS, BookingNeed, COPY_LENGTH_ORDER, CopyLength, GENERATION_LIMITS, HARNESS_SOURCE, LITTLE_MOVEMENT_AVOID, LITTLE_MOVEMENT_CAP, LintKind, MAX_ANALYSIS_MOODS, MAX_PIXEL_SAMPLES, MAX_READ_PICTURES, NEEDS, NEED_IDS, NeedSource, PALETTE_SIZE, PALETTE_THRESHOLDS, PictureRole, PlanChangeOutcome, PresetFit, RANK_WEIGHTS, READING_SHAPE, READ_ANIMATE, READ_WEIGHTS, RankReason, ReaderState, ReadingIssueCode, SITE_ANALYSIS_SCHEMA_NAME, SITE_READING_SCHEMA_NAME, SPEC_ANALYSIS_SHAPE, SPEC_BUSINESS_TYPES, SPEC_HARNESS_EXAMPLES, SPEC_HARNESS_SOURCE, SPEC_NEEDS, SPEC_STRUCTURES, SPEC_USER_AVOIDS, STRUCTURES, SiteReadRefusal, StyleReadGround, StyleReadHeadings, StyleReadMotion, StyleReadSourceKind, USER_AVOID_IDS, VoiceFormality, VoicePerson, addressesIn, analysisFromWire, analysisPromptContext, analysisToWire, businessTypeById, calmerChoice, decodeSiteReading, inferNeeds, isLookRule, lintDesign, lookMatches, mapStyleRead, matchBusinessType, matchBusinessTypes, measurePalette, mergeStyleReads, motionAdvice, motionFor, normalizeAddress, oklchHue, paletteBrand, paletteGround, paletteSamples, pictureStyleRead, pixelSamples, presetDesign, presetHasSerifHeadings, presetLook, presetReason, rankStylePresets, rankWithReferences, readPalette, referenceSeed, seedPassesPreset, siteAnalysisJsonSchema, siteReadingJsonSchema, startingDesign, structureFor, userAvoidRule, validateSiteAnalysis, validateSiteReading, withoutAddress };
|
|
2998
|
-
export type { AiGenerationView, AnalysisContext, AnalysisIssue, AnalysisValidation, AvoidRule, BusinessType, BusinessTypeMatch, CheckRule, ColourSample, FactRead, GenerationFileView, GenerationReadersView, GenerationStartRequest, GoodFit, LintFinding, LintSite, LookCondition, LookDesign, LookRule, MeasuredPicture, MotionBasis, MotionInput, MotionStart, NeedDef, NeedId, PaletteColour, PaletteReading, PictureStyle, PlanRevisionView, PromptContext, PromptContextOptions, PromptSection, RankInput, RankedPreset, ReadingIssue, ReferenceSeed, SiteAnalysis, SiteAuditView, SiteNeed, SiteReaderView, SiteReading, StartingDesign, StructureChoice, StructureId, StyleHints, StyleRead, UserAvoidId };
|
|
3013
|
+
export { ANIMATE_ADVICE_STEPS, AnalysisIssueCode, AnalysisLocale, AvoidCheck, BUSINESS_TYPES, BUSINESS_TYPE_IDS, BookingNeed, COPY_LENGTH_ORDER, CopyLength, GENERATION_LIMITS, HARNESS_SOURCE, LITTLE_MOVEMENT_AVOID, LITTLE_MOVEMENT_CAP, LintKind, MAX_ANALYSIS_MOODS, MAX_PIXEL_SAMPLES, MAX_READ_PICTURES, NEEDS, NEED_IDS, NeedSource, PALETTE_SIZE, PALETTE_THRESHOLDS, PictureRole, PlanChangeOutcome, PresetFit, RANK_WEIGHTS, READING_SHAPE, READ_ANIMATE, READ_WEIGHTS, RankReason, ReaderState, ReadingIssueCode, SITE_ANALYSIS_SCHEMA_NAME, SITE_READING_SCHEMA_NAME, SPEC_ANALYSIS_SHAPE, SPEC_BUSINESS_TYPES, SPEC_HARNESS_EXAMPLES, SPEC_HARNESS_SOURCE, SPEC_NEEDS, SPEC_STRUCTURES, SPEC_USER_AVOIDS, STRUCTURES, SiteReadRefusal, StyleReadGround, StyleReadHeadings, StyleReadMotion, StyleReadSourceKind, USER_AVOID_IDS, VoiceFormality, VoicePerson, addressesIn, analysisFromWire, analysisPromptContext, analysisToWire, bestRankedPreset, businessTypeById, calmerChoice, decodeSiteReading, inferNeeds, isLookRule, lintDesign, lookMatches, mapStyleRead, matchBusinessType, matchBusinessTypes, measurePalette, mergeStyleReads, motionAdvice, motionFor, normalizeAddress, oklchHue, paletteBrand, paletteGround, paletteSamples, pictureStyleRead, pixelSamples, presetDesign, presetHasSerifHeadings, presetLook, presetReason, presetsByFlavor, rankStylePresets, rankWithReferences, readPalette, referenceSeed, seedPassesPreset, siteAnalysisJsonSchema, siteReadingJsonSchema, startingDesign, structureFor, userAvoidRule, validateSiteAnalysis, validateSiteReading, withoutAddress };
|
|
3014
|
+
export type { AiGenerationView, AnalysisContext, AnalysisIssue, AnalysisValidation, AvoidRule, BusinessType, BusinessTypeMatch, CheckRule, ColourSample, FactRead, GenerationFileView, GenerationReadersView, GenerationStartRequest, GoodFit, LintFinding, LintSite, LookCondition, LookDesign, LookRule, MeasuredPicture, MotionBasis, MotionInput, MotionStart, NeedDef, NeedId, PaletteColour, PaletteReading, PictureStyle, PlanRevisionView, PresetFlavorGroup, PromptContext, PromptContextOptions, PromptSection, RankInput, RankedPreset, ReadingIssue, ReferenceSeed, SiteAnalysis, SiteAuditView, SiteNeed, SiteReaderView, SiteReading, StartingDesign, StructureChoice, StructureId, StyleHints, StyleRead, UserAvoidId };
|
package/package.json
CHANGED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How a dialog LEAVES (CC1 follow-up — the drawer that vanished). Opening is pure CSS (`@starting-style`
|
|
3
|
+
* in the shared `iox-design.css`); closing needs one beat of script, because `dialog.close()` drops the
|
|
4
|
+
* top layer at once and only some browsers can hold it for a transition (`overlay … allow-discrete`).
|
|
5
|
+
*
|
|
6
|
+
* The ONE exit every renderer runs: mark the element closing (`data-iox-closing` — the shared CSS gives it
|
|
7
|
+
* its closed look: the drawer back off its edge, the scrim clear), wait for the transitions that started,
|
|
8
|
+
* then finish. Reduced motion (or a stub with no animations) finishes now. Framework-free: the engine's
|
|
9
|
+
* every close path (actions, the X, the backdrop, Esc, the close-above watcher) and the builder canvas's
|
|
10
|
+
* stage share it.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export const DIALOG_CLOSING_ATTRIBUTE = 'data-iox-closing';
|
|
14
|
+
|
|
15
|
+
/** Longest an exit may hold the element before it finishes anyway — a transition that never ends (a
|
|
16
|
+
* value that did not change, a tab in the background) must not leave a dialog half-closed. */
|
|
17
|
+
export const DIALOG_EXIT_TIMEOUT_MS = 700;
|
|
18
|
+
|
|
19
|
+
export interface ExitAnimationLike {
|
|
20
|
+
finished: Promise<unknown>;
|
|
21
|
+
effect?: { getComputedTiming?: () => { endTime?: number | null } } | null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ExitMotionElement {
|
|
25
|
+
setAttribute(name: string, value: string): void;
|
|
26
|
+
removeAttribute(name: string): void;
|
|
27
|
+
hasAttribute(name: string): boolean;
|
|
28
|
+
getAnimations?: (options?: { subtree?: boolean }) => ExitAnimationLike[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface DialogElementLike extends ExitMotionElement {
|
|
32
|
+
open: boolean;
|
|
33
|
+
close(): void;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface ExitMotionOptions {
|
|
37
|
+
/** False under reduced motion: finish at once (the caller asks `motionAllowed()`). */
|
|
38
|
+
animate: boolean;
|
|
39
|
+
timeoutMs?: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** True while an exit is running on the element (a second close waits on it; an open cancels it). */
|
|
43
|
+
export function isExiting(el: ExitMotionElement | null | undefined): boolean {
|
|
44
|
+
return !!el?.hasAttribute(DIALOG_CLOSING_ATTRIBUTE);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Stop an exit that is running (the dialog was opened again mid-close): it stays open. */
|
|
48
|
+
export function cancelExit(el: ExitMotionElement | null | undefined): void {
|
|
49
|
+
el?.removeAttribute(DIALOG_CLOSING_ATTRIBUTE);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const finite = (a: ExitAnimationLike) => {
|
|
53
|
+
const end = a.effect?.getComputedTiming?.().endTime;
|
|
54
|
+
return end === undefined || end === null || Number.isFinite(end);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Run the exit: mark the element, wait for its finite animations (its subtree's too — the drawer's links,
|
|
59
|
+
* the `::backdrop`) or the timeout. Resolves TRUE when the exit completed and the caller should finish
|
|
60
|
+
* (close / remove), FALSE when it was cancelled meanwhile. The mark stays on until the caller finishes.
|
|
61
|
+
*/
|
|
62
|
+
export async function runExitMotion(el: ExitMotionElement, options: ExitMotionOptions): Promise<boolean> {
|
|
63
|
+
el.setAttribute(DIALOG_CLOSING_ATTRIBUTE, '');
|
|
64
|
+
if (!options.animate) return true;
|
|
65
|
+
// Reading the animations flushes style, so the transitions the mark just started are in the list.
|
|
66
|
+
const running = (el.getAnimations?.({ subtree: true }) ?? []).filter(finite);
|
|
67
|
+
if (running.length) {
|
|
68
|
+
const timeout = options.timeoutMs ?? DIALOG_EXIT_TIMEOUT_MS;
|
|
69
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
70
|
+
await Promise.race([
|
|
71
|
+
Promise.all(running.map(a => a.finished.catch(() => undefined))),
|
|
72
|
+
new Promise<void>(resolve => { timer = setTimeout(resolve, timeout); }),
|
|
73
|
+
]);
|
|
74
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
75
|
+
}
|
|
76
|
+
return isExiting(el);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const pending = new WeakMap<object, Promise<void>>();
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Close a live `<dialog>` with its exit motion. A second call while it is closing joins the first; an open
|
|
83
|
+
* in between (`cancelExit`) keeps it open. The dialog's own `close` event still fires once, after the
|
|
84
|
+
* motion — so whatever a runtime does on close (restart the page scroll, sync the openers, return focus)
|
|
85
|
+
* happens when the dialog is really gone.
|
|
86
|
+
*/
|
|
87
|
+
export function closeDialogAnimated(el: DialogElementLike, options: ExitMotionOptions): Promise<void> {
|
|
88
|
+
if (!el.open) return Promise.resolve();
|
|
89
|
+
const running = pending.get(el);
|
|
90
|
+
if (running && isExiting(el)) return running;
|
|
91
|
+
const done = runExitMotion(el, options).then(finish => {
|
|
92
|
+
pending.delete(el);
|
|
93
|
+
if (!finish) return;
|
|
94
|
+
if (el.open) el.close();
|
|
95
|
+
cancelExit(el);
|
|
96
|
+
});
|
|
97
|
+
pending.set(el, done);
|
|
98
|
+
return done;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** True when a toggle should CLOSE the dialog: it is open and not already on its way out. */
|
|
102
|
+
export function dialogTogglesClosed(el: Pick<DialogElementLike, 'open' | 'hasAttribute'>): boolean {
|
|
103
|
+
return el.open && !el.hasAttribute(DIALOG_CLOSING_ATTRIBUTE);
|
|
104
|
+
}
|
|
@@ -72,3 +72,16 @@ export function presetChoices(id: string, mode?: DesignMode): DesignChoices | nu
|
|
|
72
72
|
motion: preset.motion, pillButtons: preset.pillButtons, mode: wear, tint: preset.tint, ...preset.moves,
|
|
73
73
|
});
|
|
74
74
|
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The preset a set of choices IS (Design ▸ Look's "Current", 2026-10-28): its look — flavor, colour, tint, pairing,
|
|
78
|
+
* shape, spacing, pill buttons — in a mode it offers. Motion, speed and font overrides are the site's, not the
|
|
79
|
+
* style's, so they never unmark it; a colour or a corner changed by hand does: the look is then its flavor's, customised.
|
|
80
|
+
*/
|
|
81
|
+
export function stylePresetOfChoices(choices: DesignChoices | null | undefined): StylePreset | undefined {
|
|
82
|
+
if (!choices) return undefined;
|
|
83
|
+
const c = resolveDesignChoices(choices);
|
|
84
|
+
return STYLE_PRESETS.find(p => p.flavorId === c.flavorId && p.seed.toLowerCase() === c.seed.toLowerCase() && p.tint === c.tint
|
|
85
|
+
&& p.pairId === c.pairId && p.shape === c.shape && p.density === c.density && p.pillButtons === c.pillButtons
|
|
86
|
+
&& presetModes(p).includes(c.mode));
|
|
87
|
+
}
|
|
@@ -252,8 +252,18 @@ function makeKit(globals: ReadonlyArray<SerializedNode>, rootId: string, newId?:
|
|
|
252
252
|
|
|
253
253
|
/** Point an opener's dialog actions at `dialogId` (a drawer rebuilt under a new id keeps its openers working). */
|
|
254
254
|
function retarget(node: SerializedNode, dialogId: string): SerializedNode {
|
|
255
|
-
for (const ix of node.interactions as Array<{ actions?: Array<{ type?: string; target?: string }> }>) {
|
|
256
|
-
for (const a of ix.actions ?? []) if (a.type ===
|
|
255
|
+
for (const ix of (node.interactions ?? []) as Array<{ actions?: Array<{ type?: string; target?: string }> }>) {
|
|
256
|
+
for (const a of ix.actions ?? []) if (a.type === NavDialogAction.Open || a.type === NavDialogAction.Toggle) a.target = dialogId;
|
|
257
|
+
}
|
|
258
|
+
return asToggle(node);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** The menu button and More are TOGGLES (a second click closes what the first opened): a part saved before that
|
|
262
|
+
* rule opened only — kept through a change of shape, it toggles from then on. */
|
|
263
|
+
function asToggle(node: SerializedNode): SerializedNode {
|
|
264
|
+
for (const ix of (node.interactions ?? []) as Array<{ trigger?: string; actions?: Array<{ type?: string }> }>) {
|
|
265
|
+
if (ix.trigger !== CLICK) continue;
|
|
266
|
+
for (const a of ix.actions ?? []) if (a.type === NavDialogAction.Open) a.type = NavDialogAction.Toggle;
|
|
257
267
|
}
|
|
258
268
|
return node;
|
|
259
269
|
}
|
|
@@ -267,7 +277,14 @@ function part(kit: Kit, suffix: string, type: string, props: Record<string, unkn
|
|
|
267
277
|
return node;
|
|
268
278
|
}
|
|
269
279
|
|
|
270
|
-
|
|
280
|
+
/** The dialog actions a nav part runs on click. */
|
|
281
|
+
enum NavDialogAction {
|
|
282
|
+
Open = 'openDialog',
|
|
283
|
+
Close = 'closeDialog',
|
|
284
|
+
Toggle = 'toggleDialog',
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function clickTo(kit: Kit, suffix: string, type: NavDialogAction, target: string) {
|
|
271
288
|
return { id: kit.id(`${suffix}-click`), trigger: CLICK, actions: [{ type, target, duration: 0, delay: 0, easing: 'ease' }] };
|
|
272
289
|
}
|
|
273
290
|
|
|
@@ -302,11 +319,15 @@ function labelText(kit: Kit, suffix: string, words: string): SerializedNode {
|
|
|
302
319
|
return part(kit, suffix, TEXT, { content: words, tag: 'span' }, 'button-label');
|
|
303
320
|
}
|
|
304
321
|
|
|
322
|
+
/** The menu button and the drawer's Close are ONE size — and the shared CSS pads the drawer like the header
|
|
323
|
+
* (`.iox-role-nav-drawer` re-declares dialog-pad as nav-pad) — so the Close lands where the menu button is and
|
|
324
|
+
* reads as the same button turned into an X. */
|
|
325
|
+
const MENU_BUTTON_LAY = { width: '48px', height: '48px', alignItems: 'center', justifyContent: 'center' };
|
|
326
|
+
|
|
305
327
|
function menuButton(kit: Kit, drawerId: string, at: string): SerializedNode {
|
|
306
|
-
const btn = part(kit, 'menu', BUTTON, { tag: 'button', ariaLabel: NAV_SHAPE_COPY.menu }, 'button-ghost', NavRole.MenuButton,
|
|
307
|
-
{ width: '48px', height: '48px', alignItems: 'center', justifyContent: 'center' });
|
|
328
|
+
const btn = part(kit, 'menu', BUTTON, { tag: 'button', ariaLabel: NAV_SHAPE_COPY.menu }, 'button-ghost', NavRole.MenuButton, { ...MENU_BUTTON_LAY });
|
|
308
329
|
btn.children = [iconNode(kit, 'menu-icon', MENU_ICON)];
|
|
309
|
-
btn.interactions = [clickTo(kit, 'menu',
|
|
330
|
+
btn.interactions = [clickTo(kit, 'menu', NavDialogAction.Toggle, drawerId)];
|
|
310
331
|
phoneOnly(btn, at, 'inline-flex');
|
|
311
332
|
return btn;
|
|
312
333
|
}
|
|
@@ -322,10 +343,9 @@ function drawer(kit: Kit, root: SerializedNode, pages: NavPage[], at: string): S
|
|
|
322
343
|
const brandWords = textOf(find([root], NavRole.Brand) ?? root) || NAV_PART_LABELS[NavRole.Root];
|
|
323
344
|
const head = part(kit, 'drawer-head', CONTAINER, {}, 'box', NavRole.DrawerHead,
|
|
324
345
|
{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: '12px' });
|
|
325
|
-
const close = part(kit, 'drawer-close', BUTTON, { tag: 'button', ariaLabel: NAV_SHAPE_COPY.closeMenu }, 'button-ghost', NavRole.DrawerClose,
|
|
326
|
-
{ width: '44px', height: '44px', alignItems: 'center', justifyContent: 'center' });
|
|
346
|
+
const close = part(kit, 'drawer-close', BUTTON, { tag: 'button', ariaLabel: NAV_SHAPE_COPY.closeMenu }, 'button-ghost', NavRole.DrawerClose, { ...MENU_BUTTON_LAY });
|
|
327
347
|
close.children = [iconNode(kit, 'drawer-close-icon', CLOSE_ICON)];
|
|
328
|
-
close.interactions = [clickTo(kit, 'drawer-close',
|
|
348
|
+
close.interactions = [clickTo(kit, 'drawer-close', NavDialogAction.Close, dr.id)];
|
|
329
349
|
head.children = [part(kit, 'drawer-brand', HEADING, { content: brandWords, level: 'h3' }, 'h3'), close];
|
|
330
350
|
const list = part(kit, 'drawer-links', CONTAINER, {}, 'box', NavRole.DrawerLinks,
|
|
331
351
|
{ display: 'flex', flexDirection: 'column', gap: '4px' });
|
|
@@ -364,7 +384,7 @@ const BAR_ITEM_LAY = {
|
|
|
364
384
|
function moreItem(kit: Kit, drawerId: string): SerializedNode {
|
|
365
385
|
const more = part(kit, 'bar-more', BUTTON, { tag: 'button' }, 'bottom-bar-item', NavRole.More, BAR_ITEM_LAY);
|
|
366
386
|
more.children = [iconNode(kit, 'bar-more-icon', MORE_ICON), labelText(kit, 'bar-more-label', NAV_SHAPE_COPY.more)];
|
|
367
|
-
more.interactions = [clickTo(kit, 'bar-more',
|
|
387
|
+
more.interactions = [clickTo(kit, 'bar-more', NavDialogAction.Toggle, drawerId)];
|
|
368
388
|
return more;
|
|
369
389
|
}
|
|
370
390
|
|
|
@@ -472,7 +492,7 @@ export function planNavShape(globals: ReadonlyArray<SerializedNode>, shape: NavS
|
|
|
472
492
|
|
|
473
493
|
// The menu button: Drawer only (Q4: with a bar, the phone header keeps only the brand).
|
|
474
494
|
const existingMenu = removeRole(root, NavRole.MenuButton);
|
|
475
|
-
const menu = wantMenuButton ? (existingMenu
|
|
495
|
+
const menu = wantMenuButton ? (existingMenu ? asToggle(existingMenu) : menuButton(kit, dr!.id, at)) : null;
|
|
476
496
|
|
|
477
497
|
for (const p of phoneHiddenParts(root)) { moveDisplayOverride(p, at); hideAt(p, at); }
|
|
478
498
|
centerBrandAt(root, at, wantBar);
|
package/src/harness-core/rank.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
-
DensityId, flavorPackById, MOOD_LABELS, MoodId, parseFontFamilies, ShapeId, STYLE_PRESETS, StylePreset, TintId,
|
|
2
|
+
DensityId, flavorPackById, MOOD_LABELS, MoodId, parseFontFamilies, ShapeId, SPEC_FLAVORS, STYLE_PRESETS, StylePreset, TintId,
|
|
3
3
|
} from '@vectoriox/iox-ui/design-core';
|
|
4
4
|
import type { AnimateId } from '@vectoriox/iox-ui/design-core';
|
|
5
5
|
|
|
@@ -133,6 +133,35 @@ export function presetReason(ranked: RankedPreset, preset: StylePreset | undefin
|
|
|
133
133
|
return (hebrew ? preset?.blurbHe : preset?.blurb) ?? '';
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
+
/** One flavor's styles in Design ▸ Look: its presets in ranking order. */
|
|
137
|
+
export interface PresetFlavorGroup {
|
|
138
|
+
flavorId: string;
|
|
139
|
+
presets: RankedPreset[];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Design ▸ Look's grid (decided 2026-10-28, Doron: "All 24, grouped by flavor" — reverses Q7 and the wizard's best 6):
|
|
144
|
+
* every ranked style under its flavor, the flavors in catalogue order (Crisp · Linen · Midnight · Glass · Bloom · Pop),
|
|
145
|
+
* each flavor's styles best-ranked first. A ranking with no basis (no business, no moods) is catalogue order, so the
|
|
146
|
+
* groups are too. One rule for both hosts.
|
|
147
|
+
*/
|
|
148
|
+
export function presetsByFlavor(
|
|
149
|
+
ranked: ReadonlyArray<RankedPreset>,
|
|
150
|
+
presets: ReadonlyArray<StylePreset> = STYLE_PRESETS,
|
|
151
|
+
flavors: ReadonlyArray<{ id: string }> = SPEC_FLAVORS,
|
|
152
|
+
): PresetFlavorGroup[] {
|
|
153
|
+
const flavorOf = new Map(presets.map(p => [p.id, p.flavorId]));
|
|
154
|
+
return flavors
|
|
155
|
+
.map(f => ({ flavorId: f.id, presets: ranked.filter(r => flavorOf.get(r.id) === f.id) }))
|
|
156
|
+
.filter(g => g.presets.length > 0);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** The style the ranking puts first — only when it had something to rank by (a positive score); else none is "best". */
|
|
160
|
+
export function bestRankedPreset(ranked: ReadonlyArray<RankedPreset>): string | null {
|
|
161
|
+
const top = ranked[0];
|
|
162
|
+
return top && top.score > 0 ? top.id : null;
|
|
163
|
+
}
|
|
164
|
+
|
|
136
165
|
// ── References: a reading of pictures or a site, as style (plan §3k, R4) ─────────────────────────────────
|
|
137
166
|
export enum StyleReadSourceKind {
|
|
138
167
|
Image = 'image',
|
package/styles/iox-design.css
CHANGED
|
@@ -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).
|
|
122
|
-
|
|
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[
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|