diff --git a/src/shared/versions.ts b/src/shared/versions.ts index 5688697..9682f8b 100644 --- a/src/shared/versions.ts +++ b/src/shared/versions.ts @@ -1,6 +1,6 @@ export const HARBOR_VERSIONS = Object.freeze({ - macClient: '0.26.1', - gatewayClient: '0.27.1', + macClient: '0.26.2', + gatewayClient: '0.27.2', gatewayBackend: '0.27.0', }); diff --git a/src/web/features/routing/RoutingFeature.tsx b/src/web/features/routing/RoutingFeature.tsx index 46dc029..2f59644 100644 --- a/src/web/features/routing/RoutingFeature.tsx +++ b/src/web/features/routing/RoutingFeature.tsx @@ -12,6 +12,7 @@ import { import { flushSync } from 'react-dom'; import { canAppendRouteRule, + normalizeRouteRules, ROUTE_RULES_CONTRACT_VERSION, } from '../../../shared/routingRules.js'; import type { RouteRule } from '../../../shared/contracts/state.js'; @@ -112,6 +113,14 @@ const sameRule = (left?: RouteRule, right?: RouteRule) => ( Boolean(left && right && localRuleKey(left) === localRuleKey(right)) ); +const normalizeDraftRuleValue = (rule: DraftRule, value: string) => { + try { + return normalizeRouteRules([{ ...rule, value }], { strict: true })[0]?.value || value; + } catch { + return value; + } +}; + function localRuleStatus( rule: DraftRule, index: number, @@ -382,27 +391,28 @@ export function useRoutingFeature({ function positionPointerRule(pointerY: number) { const session = dragRef.current; if (!session?.lifted) return; + const list = panelRef.current?.querySelector('.client-local-rules-list'); + if (!list) return; const rows = ruleRows(); let row = rows.get(session.key); if (!row) return; - let rect = row.getBoundingClientRect(); const desiredCenter = pointerY - session.pointerOffsetY; - const baseCenter = rect.top + rect.height / 2 - session.translateY; - session.translateY = desiredCenter - baseCenter; + const listTop = list.getBoundingClientRect().top; + const slotCenter = (element: HTMLElement) => listTop + element.offsetTop + element.offsetHeight / 2; + session.translateY = desiredCenter - slotCenter(row); row.style.setProperty('--client-rule-drag-y', `${session.translateY}px`); const currentIndex = rulesRef.current.findIndex((rule) => rule._key === session.key); const centers = rulesRef.current.map((rule) => { - const ruleRect = rows.get(rule._key)?.getBoundingClientRect(); - return ruleRect ? ruleRect.top + ruleRect.height / 2 : desiredCenter; + const ruleRow = rows.get(rule._key); + return ruleRow ? slotCenter(ruleRow) : desiredCenter; }); const target = crossedRuleIndex(currentIndex, desiredCenter, centers); if (target === currentIndex) return; moveRuleTo(session.key, target); row = ruleRows().get(session.key); if (!row) return; - rect = row.getBoundingClientRect(); - session.translateY = desiredCenter - (rect.top + rect.height / 2 - session.translateY); + session.translateY = desiredCenter - slotCenter(row); row.style.setProperty('--client-rule-drag-y', `${session.translateY}px`); setReorderAnnouncement(`Правило перемещено на позицию ${target + 1}`); } @@ -896,6 +906,14 @@ export function RoutingPanel({ feature, statusSlot }: { feature: RoutingFeature; Правила сохранены и начнут работать после запуска Harbor.

)} +
+ Как работают правила +
+

Правила проверяются сверху вниз. Первое совпадение выбирает маршрут: щит означает VPN, стрелка — прямое подключение.

+

Правила применяются только к трафику внутри маршрутизации Harbor. Gateway в режиме «Напрямую» и Connect при активном Harbor Gateway обходят локальный список.

+

Можно вставить полный URL: Harbor сразу оставит только домен. Путь и параметры HTTPS для маршрутизации недоступны.

+
+
@@ -928,6 +946,7 @@ export function RoutingPanel({ feature, statusSlot }: { feature: RoutingFeature; className={`client-local-rule client-deletable-row is-${status}${rule.enabled ? '' : ' is-disabled'}${rule.removing ? ' is-removing' : ''}${lifted ? ' is-dragging' : ''}`} key={rule._key} data-rule-key={rule._key} + data-route={rule.outbound} role="listitem" style={{ viewTransitionName: rule.removing ? 'none' : rule._key }} inert={rule.removing ? true : undefined} @@ -954,20 +973,23 @@ export function RoutingPanel({ feature, statusSlot }: { feature: RoutingFeature; - + + + {rule.enabled ? 'Отключить правило' : 'Включить правило'} + feature.change(index, 'value', event.target.value)} + onPaste={(event) => { + event.preventDefault(); + feature.change(index, 'value', normalizeDraftRuleValue(rule, event.clipboardData.getData('text'))); + }} + onBlur={() => feature.change(index, 'value', normalizeDraftRuleValue(rule, rule.value))} /> feature.remove(rule._key)} > - × + -

- Правила применяются только к трафику, который вошёл в VPN-маршрутизацию Harbor. Устройство Gateway в режиме «Напрямую» и Connect при активном Harbor Gateway обходят локальный список; «Напрямую» внутри правила — результат уже найденного совпадения. -

-

- Можно вставить полный URL: Harbor сохранит только домен. Путь и параметры HTTPS недоступны для маршрутизации. -

{statusSlot}
diff --git a/src/web/styles/features/routing.css b/src/web/styles/features/routing.css index 797651b..374e676 100644 --- a/src/web/styles/features/routing.css +++ b/src/web/styles/features/routing.css @@ -102,6 +102,7 @@ .client-local-rule { --client-rule-drag-y: 0px; + --client-rule-route-color: var(--client-accent); position: relative; isolation: isolate; min-width: 0; @@ -110,8 +111,28 @@ align-items: center; column-gap: 6px; padding: 4px 0; + background: linear-gradient(90deg, color-mix(in oklch, var(--client-rule-route-color) 8%, transparent), transparent 58%); animation: client-row-enter 560ms cubic-bezier(0.16, 1, 0.3, 1) both; - transition: background-color 180ms ease, box-shadow 180ms ease, opacity 260ms ease, filter 360ms ease; + transition: background 320ms ease, box-shadow 180ms ease, opacity 260ms ease, filter 360ms ease; +} + +.client-local-rule[data-route='direct'] { + --client-rule-route-color: light-dark(oklch(0.52 0.11 240), oklch(0.76 0.1 240)); +} + +.client-local-rule::after { + content: ''; + position: absolute; + top: 10px; + bottom: 10px; + left: 0; + width: 2px; + border-radius: 999px; + background: var(--client-rule-route-color); + box-shadow: 0 0 7px color-mix(in oklch, var(--client-rule-route-color) 44%, transparent); + opacity: 0.78; + pointer-events: none; + transition: background 320ms ease, box-shadow 320ms ease, opacity 260ms ease; } .client-local-rule + .client-local-rule::before { @@ -140,23 +161,46 @@ width: 6px; height: 6px; border-radius: 50%; - background: currentColor; + border: 1px solid currentColor; + background: transparent; opacity: 0.58; - transition: color 280ms ease, opacity 280ms ease, filter 360ms ease, transform 280ms cubic-bezier(0.16, 1, 0.3, 1); + transition: color 280ms ease, border 280ms ease, border-radius 280ms ease, background 280ms ease, opacity 280ms ease, filter 360ms ease, transform 280ms cubic-bezier(0.16, 1, 0.3, 1); } .client-local-rule.is-active .client-rule-status-dot { color: var(--client-accent); + border: 0; + background: currentColor; opacity: 1; filter: drop-shadow(0 0 5px color-mix(in oklch, var(--client-accent) 52%, transparent)); transform: scale(1.14); } -.client-local-rule.is-pending .client-rule-status-dot, .client-local-rule.is-unsaved .client-rule-status-dot { + width: 7px; + height: 7px; + border: 1.5px solid currentColor; + border-radius: 1px; color: var(--client-warning, oklch(0.72 0.12 72)); opacity: 1; - filter: drop-shadow(0 0 4px color-mix(in oklch, var(--client-warning, oklch(0.72 0.12 72)) 44%, transparent)); + transform: rotate(45deg); +} + +.client-local-rule.is-pending .client-rule-status-dot { + width: 8px; + height: 8px; + border: 1.5px dashed currentColor; + color: var(--client-warning, oklch(0.72 0.12 72)); + opacity: 1; +} + +.client-local-rule.is-disabled .client-rule-status-dot { + width: 8px; + height: 2px; + border: 0; + border-radius: 999px; + background: var(--client-muted); + transform: none; } .client-local-rule.is-disabled > :not(.client-rule-handle):not(.client-row-delete):not(.client-delete-strike) { @@ -164,11 +208,16 @@ filter: saturate(0); } +.client-local-rule.is-disabled::after { + opacity: 0.22; + filter: saturate(0); +} + .client-local-rule.is-dragging { z-index: 6; animation: none; - background: color-mix(in oklch, var(--client-accent) 5%, transparent); - box-shadow: 0 10px 28px color-mix(in oklch, var(--client-accent) 12%, transparent); + background: linear-gradient(90deg, color-mix(in oklch, var(--client-rule-route-color) 13%, transparent), transparent 72%); + box-shadow: 0 10px 28px color-mix(in oklch, var(--client-rule-route-color) 16%, transparent); transform: translateY(var(--client-rule-drag-y)); } @@ -177,6 +226,13 @@ animation: client-row-leave 820ms cubic-bezier(0.7, 0, 0.84, 0) both; } +.client-local-rule-enabled-wrap { + width: 24px; + height: 44px; + display: grid; + place-items: center; +} + .client-local-rule-enabled { width: 24px; height: 32px; @@ -228,43 +284,42 @@ } .client-local-rule-enabled svg { - width: 18px; - height: 18px; + width: 20px; + height: 20px; overflow: visible; fill: none; - stroke: currentColor; - stroke-linecap: round; - stroke-linejoin: round; transition: color 260ms ease, filter 360ms ease, transform 420ms cubic-bezier(0.16, 1, 0.3, 1); } -.client-local-rule-enabled circle { - stroke-width: 1.4; - transition: fill 320ms ease, stroke 260ms ease; +.client-rule-switch-track { + fill: color-mix(in oklch, var(--client-muted) 16%, transparent); + stroke: currentColor; + stroke-width: 1; + transition: fill 260ms ease, stroke 260ms ease; } -.client-rule-check { - stroke-width: 1.8; - stroke-dasharray: 12; - stroke-dashoffset: 12; - transition: opacity 160ms ease, stroke-dashoffset 360ms cubic-bezier(0.16, 1, 0.3, 1); +.client-rule-switch-thumb { + fill: currentColor; + stroke: none; + transform-box: fill-box; + transform-origin: center; + transition: transform 320ms cubic-bezier(0.16, 1, 0.3, 1); } .client-local-rule-enabled[aria-checked='true'] { - color: var(--client-accent); + color: var(--client-rule-route-color); } .client-local-rule-enabled[aria-checked='true'] svg { - filter: drop-shadow(0 0 7px color-mix(in oklch, var(--client-accent) 48%, transparent)); - transform: scale(1.08); + filter: drop-shadow(0 0 7px color-mix(in oklch, var(--client-rule-route-color) 42%, transparent)); } -.client-local-rule-enabled[aria-checked='true'] circle { - fill: color-mix(in oklch, var(--client-accent) 12%, transparent); +.client-local-rule-enabled[aria-checked='true'] .client-rule-switch-track { + fill: color-mix(in oklch, var(--client-rule-route-color) 20%, transparent); } -.client-local-rule-enabled[aria-checked='true'] .client-rule-check { - stroke-dashoffset: 0; +.client-local-rule-enabled[aria-checked='true'] .client-rule-switch-thumb { + transform: translateX(8px); } .client-rule-type { @@ -422,15 +477,11 @@ border: 0; border-radius: 50%; background: transparent; - color: var(--client-muted); + color: var(--client-rule-route-color); cursor: pointer; transition: color 220ms ease, filter 320ms ease, background 220ms ease; } -.client-rule-outbound button.is-direct { - color: var(--client-accent); -} - .client-rule-outbound svg { width: 20px; height: 20px; @@ -448,15 +499,13 @@ .client-rule-outbound button:focus-visible { outline: 0; - background: color-mix(in oklch, var(--client-accent) 10%, transparent); - color: var(--client-accent); - filter: drop-shadow(0 0 5px color-mix(in oklch, var(--client-accent) 36%, transparent)); + background: color-mix(in oklch, var(--client-rule-route-color) 10%, transparent); + filter: drop-shadow(0 0 5px color-mix(in oklch, var(--client-rule-route-color) 36%, transparent)); } .client-rule-outbound button:hover:not(:disabled) { - background: color-mix(in oklch, var(--client-accent) 10%, transparent); - color: var(--client-accent); - filter: drop-shadow(0 0 5px color-mix(in oklch, var(--client-accent) 36%, transparent)); + background: color-mix(in oklch, var(--client-rule-route-color) 10%, transparent); + filter: drop-shadow(0 0 5px color-mix(in oklch, var(--client-rule-route-color) 36%, transparent)); } .client-rule-outbound button.is-direct:hover:not(:disabled) svg, @@ -490,8 +539,42 @@ text-transform: var(--type-control-transform); } -.client-local-rules-note { +.client-local-rules-help { + grid-column: 1 / -1; + margin-top: 4px; +} + +.client-local-rules-help summary { + width: fit-content; + color: var(--client-muted); + font: var(--type-label); + letter-spacing: var(--type-label-tracking); + text-transform: var(--type-label-transform); + cursor: pointer; + transition: color 220ms ease, text-shadow 300ms ease; +} + +.client-local-rules-help summary:hover, +.client-local-rules-help summary:focus-visible { + outline: 0; + color: var(--client-accent); + text-shadow: 0 0 9px color-mix(in oklch, var(--client-accent) 34%, transparent); +} + +.client-local-rules-help > div { max-width: 48ch; + display: grid; + gap: 8px; + margin-top: 10px; + padding: 10px 12px; + border-radius: 10px; + background: color-mix(in oklch, var(--client-accent) 5%, transparent); + animation: client-local-rules-notice 280ms ease both; +} + +.client-local-rules-help > div p { + max-width: 48ch; + margin: 0; color: var(--client-muted); font: var(--type-label); letter-spacing: var(--type-label-tracking); diff --git a/src/web/styles/layout.css b/src/web/styles/layout.css index bc90d03..e7f960e 100644 --- a/src/web/styles/layout.css +++ b/src/web/styles/layout.css @@ -710,7 +710,7 @@ } .client-rule-handle, - .client-local-rule-enabled, + .client-local-rule-enabled-wrap, .client-row-delete { width: 44px; height: 44px; @@ -752,7 +752,7 @@ grid-row: 1; } - .client-local-rule-enabled { + .client-local-rule-enabled-wrap { grid-column: 2; grid-row: 1; } diff --git a/src/web/styles/primitives.css b/src/web/styles/primitives.css index e12b46a..7922d76 100644 --- a/src/web/styles/primitives.css +++ b/src/web/styles/primitives.css @@ -287,12 +287,11 @@ z-index: 0; } -.client-deletable-row.is-removing > :not(.client-delete-strike):not(.client-local-rule-enabled) { +.client-deletable-row.is-removing > :not(.client-delete-strike) { animation: client-delete-content-dim 820ms ease-out both; } -.client-deletable-row.is-removing .client-local-rule-enabled circle, -.client-deletable-row.is-removing .client-rule-check { +.client-deletable-row.is-removing .client-rule-switch-thumb { opacity: 0; } @@ -311,9 +310,33 @@ .client-row-delete:hover { color: oklch(0.68 0.15 28); filter: drop-shadow(0 0 7px oklch(0.68 0.15 28 / 0.42)); +} + +.client-row-delete:hover:not(:has(svg)) { transform: rotate(8deg) scale(1.1); } +.client-row-delete svg { + width: 17px; + height: 17px; + overflow: visible; + fill: none; + stroke: currentColor; + stroke-width: 1.6; + stroke-linecap: round; + stroke-linejoin: round; +} + +.client-row-delete-lid { + transform-origin: center 7px; + transition: transform 260ms cubic-bezier(0.16, 1, 0.3, 1); +} + +.client-row-delete:hover:not(:disabled) .client-row-delete-lid, +.client-row-delete:focus-visible .client-row-delete-lid { + transform: translateY(-2px) rotate(-8deg); +} + .client-rule-handle:focus-visible, .client-local-rule-enabled:focus-visible, .client-rule-type-trigger:focus-visible, diff --git a/src/web/styles/themes.css b/src/web/styles/themes.css index 7a26c66..6bd4a85 100644 --- a/src/web/styles/themes.css +++ b/src/web/styles/themes.css @@ -122,10 +122,11 @@ } .client-local-rule, + .client-local-rule::after, .client-rule-handle, .client-local-rule-enabled svg, - .client-local-rule-enabled circle, - .client-local-rule-enabled path, + .client-rule-switch-track, + .client-rule-switch-thumb, .client-rule-type-trigger, .client-rule-type-trigger svg, .client-rule-type-list, @@ -135,9 +136,13 @@ .client-rule-outbound svg, .client-rule-status-dot, .client-row-delete, + .client-row-delete svg, + .client-row-delete-lid, .client-row-add, .client-row-add-slot > span, .client-local-rules-save, + .client-local-rules-help summary, + .client-local-rules-help > div, .client-local-rules-runtime, .client-local-rules-actions button { transition: none; diff --git a/test/web/responsive-layout-contract.test.js b/test/web/responsive-layout-contract.test.js index 26c4c5f..eadff38 100644 --- a/test/web/responsive-layout-contract.test.js +++ b/test/web/responsive-layout-contract.test.js @@ -203,7 +203,8 @@ test('routing rules stay in one desktop row and reflow without shrinking drag ta assert.match(mobile, /\.client-local-rule input \{[\s\S]*grid-row:\s*2/); assert.match(mobile, /\.client-rule-outbound \{[\s\S]*grid-column:\s*4;[\s\S]*grid-row:\s*1/); assert.match(mobile, /\.client-local-rule-status \{[\s\S]*grid-column:\s*5;[\s\S]*grid-row:\s*2/); - assert.match(mobile, /\.client-rule-handle,[\s\S]*width:\s*44px;[\s\S]*height:\s*44px/); + assert.match(mobile, /\.client-rule-handle,[\s\S]*\.client-local-rule-enabled-wrap,[\s\S]*width:\s*44px;[\s\S]*height:\s*44px/); + assert.match(mobile, /\.client-local-rule-enabled-wrap \{[\s\S]*grid-column:\s*2;[\s\S]*grid-row:\s*1/); assert.match(mobile, /\.client-rule-type-trigger,[\s\S]*\.client-rule-outbound button \{[\s\S]*height:\s*44px/); assert.match(mobile, /\.client-rule-type-list button \{[\s\S]*min-height:\s*44px/); const compact = layoutStyles.slice(layoutStyles.indexOf('@media (max-width: 360px)')); diff --git a/test/web/routing-feature-contract.test.js b/test/web/routing-feature-contract.test.js index 59a13da..2883238 100644 --- a/test/web/routing-feature-contract.test.js +++ b/test/web/routing-feature-contract.test.js @@ -46,6 +46,8 @@ test('routing controller owns ordered outbound drafts, capability gating and dra assert.match(feature, /onLostPointerCapture=\{feature\.losePointerReorder\}/); assert.match(feature, /keyboardEvent\.preventDefault\(\);[\s\S]*cancelReorder\(\)/); assert.match(feature, /stopAutoScroll\(session\)[\s\S]*Перемещение отменено/); + assert.match(feature, /const slotCenter = \(element: HTMLElement\) => listTop \+ element\.offsetTop \+ element\.offsetHeight \/ 2/); + assert.match(feature, /const centers = rulesRef\.current\.map[\s\S]*slotCenter\(ruleRow\)/); }); test('routing lifecycle and Page orchestration keep the existing guards and blocking scopes', () => { diff --git a/test/web/rule-editor-contract.test.js b/test/web/rule-editor-contract.test.js index 2dc7cb5..1180d56 100644 --- a/test/web/rule-editor-contract.test.js +++ b/test/web/rule-editor-contract.test.js @@ -32,7 +32,9 @@ test('rule editor add latency stays constant and dirty exits are guarded', () => assert.match(routing, /client-deletable-row/); assert.match(routing, /className="client-delete-strike"[\s\S]*onAnimationEnd/); assert.doesNotMatch(routing, /client-rule-delete-cross/); - assert.match(routing, /className="client-row-delete"/); + assert.match(routing, /className="client-row-delete"[\s\S]*client-row-delete-lid/); + assert.match(styles, /\.client-row-delete-lid \{[\s\S]*transition: transform/); + assert.match(styles, /\.client-row-delete:hover:not\(:disabled\) \.client-row-delete-lid,[\s\S]*translateY\(-2px\) rotate\(-8deg\)/); assert.match(styles, /\.client-deletable-row\.is-removing > \.client-delete-strike[\s\S]*client-delete-strike/); assert.match(styles, /\.client-delete-strike \{[\s\S]*z-index: 100[\s\S]*background: transparent/); assert.match(styles, /\.client-deletable-row\.is-removing > :not\(\.client-delete-strike\)[\s\S]*z-index: 0/); @@ -96,7 +98,7 @@ test('copy feedback, drawers and Gateway access actions expose complete semantic assert.doesNotMatch(component, /client-access-tabs|role="tab"|role="tabpanel"/); assert.match(styles, /\.client-drawer-close \{[\s\S]*width: 44px;[\s\S]*height: 44px/); assert.match(styles, /@media \(max-width: 560px\)[\s\S]*\.client-copy-button \{[\s\S]*min-height: 44px/); - assert.match(styles, /@media \(max-width: 560px\)[\s\S]*\.client-rule-handle,[\s\S]*\.client-local-rule-enabled,[\s\S]*\.client-row-delete \{[\s\S]*width: 44px;[\s\S]*height: 44px/); + assert.match(styles, /@media \(max-width: 560px\)[\s\S]*\.client-rule-handle,[\s\S]*\.client-local-rule-enabled-wrap,[\s\S]*\.client-row-delete \{[\s\S]*width: 44px;[\s\S]*height: 44px/); }); test('ordered rules use one accessible drag handle and a compact icon route control', () => { @@ -114,6 +116,14 @@ test('ordered rules use one accessible drag handle and a compact icon route cont assert.doesNotMatch(routing, /client-rule-outbound" role="group"/); assert.match(routing, /className=\{`client-local-rules-list\$\{draftRules\.length > 1 \? ' has-order-flow' : ''\}`\}[\s\S]*aria-label="Правила применяются сверху вниз"/); assert.match(routing, /className="client-rule-status-dot"/); + assert.match(routing, /role="switch"[\s\S]*aria-checked=\{rule\.enabled\}[\s\S]*client-rule-switch-track[\s\S]*client-rule-switch-thumb/); + assert.match(routing, /Отключить правило'[\s\S]*Включить правило'[\s\S]*/); + assert.match(routing, /data-route=\{rule\.outbound\}/); + assert.match(styles, /\.client-local-rule\[data-route='direct'\] \{[\s\S]*--client-rule-route-color/); + assert.match(styles, /\.client-local-rule::after \{[\s\S]*background: var\(--client-rule-route-color\)/); + assert.match(styles, /\.client-local-rule\.is-active \.client-rule-status-dot \{[\s\S]*background: currentColor/); + assert.match(styles, /\.client-local-rule\.is-unsaved \.client-rule-status-dot \{[\s\S]*border-radius: 1px[\s\S]*rotate\(45deg\)/); + assert.match(styles, /\.client-local-rule\.is-pending \.client-rule-status-dot \{[\s\S]*border: 1\.5px dashed/); assert.doesNotMatch(routing, />\s*[↑↓]\s* { + assert.match(routing, /normalizeRouteRules\(\[\{ \.\.\.rule, value \}\], \{ strict: true \}\)/); + assert.match(routing, /onPaste=\{\(event\) => \{[\s\S]*event\.preventDefault\(\)[\s\S]*normalizeDraftRuleValue\(rule, event\.clipboardData\.getData\('text'\)\)/); + assert.match(routing, /onBlur=\{\(\) => feature\.change\(index, 'value', normalizeDraftRuleValue\(rule, rule\.value\)\)\}/); + assert.match(routing, /
[\s\S]*[\s\S]*Как работают правила[\s\S]* div \{[\s\S]*background: color-mix/); +}); diff --git a/test/web/style-boundaries.test.js b/test/web/style-boundaries.test.js index 8fc46df..8c2b209 100644 --- a/test/web/style-boundaries.test.js +++ b/test/web/style-boundaries.test.js @@ -37,26 +37,26 @@ const expectedImports = [ const sha256 = (value) => crypto.createHash('sha256').update(value).digest('hex'); const acceptedLedger = { counts: { - cascadeEdges: 927, - customProperties: 106, - declarations: 3436, + cascadeEdges: 949, + customProperties: 108, + declarations: 3503, important: 0, keyframes: 49, media: 13, - rules: 971, - variableReferences: 834, + rules: 984, + variableReferences: 844, }, hashes: { - cascadeEdges: 'ea513313fc4cdfa85a5de74c76bdf06a00df1fc8f71825053b05f06684c7d6df', - customProperties: '06f39794d72566c2b2f8e4a2354866a54dbdceac0a1fdf9ae2619aae9abf2f36', - declarations: '0109c86e26e22898aec17aca2cf952edb88748499735c0fd2fd2163d7dd37dda', + cascadeEdges: '4c0a4c67d60c95181f4267225cbcba7e24eac3355be5e6707db9ed947a4fb54f', + customProperties: '3b97add4c3d685a532afff5046ddbe2b6eebcc0866c62781348559897f2930fb', + declarations: 'f630814428eeb4fef56795e2b8323c0fd567bc3d2334c5603973893d2a54c9f7', duplicateKeyframes: '4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945', - duplicateSelectors: '8982145dba05b33bf1c93b2d54cfbe76305b276ff3c657aa020144016ada5848', + duplicateSelectors: 'e87ccd4a057619f7ae0791564b50867d47473121bf0b0468cf1aceeafb806a1a', keyframes: '9e78309512ed82b1e9f87c58aeff505dfcdb694fc30c8f54570d01dce13eb51a', - ruleDeclarationSequences: '60267f6fbf59602e9028a9be4d8b591e1cd3e999eb5b86ab4e129199fd1853ea', - selectors: '9970cb8a580a9c57a3f39f7a801bef08e76a905ba8042deae58ac637b9769781', - variableReferences: '54bf2dffb86a0c9fc42c3d309cada316f446b17d8e54ce3c89b1108adb6e3db6', - witnesses: '0db2c977cfd7cd86209a972e02bcbca9c22722155a88ba789ccf2a395eae4cfa', + ruleDeclarationSequences: 'b6105ca3e89ca8a4b0f1c3b7e1ed7d1556da7afc856db19d3b36cc48e2de96b0', + selectors: 'd46d8c4b327f0c7ad2684ee05bf2338271c648fe0548315331c2a8719632e424', + variableReferences: '74c781fe61c4ca9734dc76415d4335612e0a0d7c26b3b3731d1bc0bd3c870b5b', + witnesses: 'c1248557ba723246417861681ca79c407b3aa47aee1b5e45fb464fb9c135c711', }, }; @@ -209,7 +209,7 @@ test('client typography uses the shared semantic scale outside the token owner', test('accepted stylesheet has pinned declaration, selector, keyframe, variable, and cascade ledgers', () => { const witnesses = readStyleWitnesses(root); - assert.equal(witnesses.length, 845); + assert.equal(witnesses.length, 854); assert.equal(witnesses.filter((witness) => witness.unknown || witness.ancestorUnknown).length, 0); const ledger = createStyleLedger(readStyleSource(root), { witnesses }); assert.deepEqual(ledger.counts, acceptedLedger.counts); @@ -405,8 +405,8 @@ test('main owns one public stylesheet and the regrouped production CSS is determ assert.equal((main.match(/import ['"][^'"]+\.css['"]/g) || []).length, 1); const assets = fs.readdirSync(path.join(root, 'dist/assets')).filter((file) => file.endsWith('.css')); - assert.deepEqual(assets, ['index-D1sDzEdb.css']); + assert.deepEqual(assets, ['index-Ch0T5A1e.css']); const built = fs.readFileSync(path.join(root, 'dist/assets', assets[0])); - assert.equal(built.byteLength, 133160); - assert.equal(sha256(built), '165e6706c69d737a09b9cdde9d8211e94dbea49b3df42c8e5c9d194993e6b618'); + assert.equal(built.byteLength, 135594); + assert.equal(sha256(built), '8425efe616036b5b37f0acc7476c1ef01c0c5168bb5e531863e498ca20a8dfa8'); }); diff --git a/test/web/ui-foundations-contract.test.js b/test/web/ui-foundations-contract.test.js index bfacc2d..6365163 100644 --- a/test/web/ui-foundations-contract.test.js +++ b/test/web/ui-foundations-contract.test.js @@ -32,7 +32,7 @@ test('all repeated client primitive consumers use the shared owners', () => { .map(({ source }) => source) .join('\n'); - assert.equal((production.match(/