Refine Harbor device ecosystem guidance and device panel layout
Build and Deploy Gateway / build-and-push (push) Successful in 21s
Build and Deploy Gateway / deploy (push) Successful in 6s

This commit is contained in:
2026-08-14 15:38:17 +03:00
parent 32217f4d17
commit a9eca0e9d4
10 changed files with 247 additions and 94 deletions
@@ -1,85 +1,70 @@
--- ---
name: design-harbor-device-ecosystem name: design-harbor-device-ecosystem
description: [TODO: Complete and informative explanation of what the skill does and when to use it. Include WHEN to use this skill - specific scenarios, file types, or tasks that trigger it.] description: Design, plan, implement, review, or document Harbor Gateway integrations with current and future companion devices. Use for LAN advertisement and discovery, Connect-Gateway pairing, trusted device identity, connected-client presence, ecosystem membership in the Gateway device list, multi-Gateway selection, capability or protocol negotiation, and related API, persistence, migration, or UI contracts. Do not use for traffic-only LAN inventory or visual polish that does not change ecosystem behavior.
--- ---
# Design Harbor Device Ecosystem # Design Harbor Device Ecosystem
## Overview Keep discovery, trust, application presence, routing, and LAN observation separate. A device is not trusted because mDNS or the neighbor table reports it, and a paired device is not necessarily connected or routed through this Gateway.
[TODO: 1-2 sentences explaining what this skill enables] ## Scope and non-goals
## Structuring This Skill - Apply this skill to Gateway integration with Harbor Connect on macOS and future clients, extensions, appliances, or companion services.
- Keep one platform-neutral protocol core. Isolate native DNS-SD and private-key storage behind thin platform adapters, and state whether a new target is desktop, headless, mobile, or browser-bound before choosing an adapter.
- Preserve the existing MAC-based inventory as an observational traffic and policy surface; do not turn it into ecosystem identity.
- Do not introduce a generic event bus, mesh, cloud account, fleet controller, or WAN discovery without a concrete requirement.
- Do not define a new visible pairing flow, badge, layout, wording, or management surface without the exact owner decision required by `workpack/DESIGN_OWNER_POLICY.md`.
- Do not deploy, SSH, pair live devices, or operate a live Gateway unless the current user request explicitly authorizes that exact operation.
[TODO: Choose the structure that best fits this skill's purpose. Common patterns: ## Workflow
**1. Workflow-Based** (best for sequential processes) 1. Read `PRODUCT.md`, `workpack/PRODUCT_PRIORITIES.md`, `workpack/DATA_CONSISTENCY_MODEL.md`, `workpack/DESIGN_OWNER_POLICY.md`, `workpack/STATUS.md`, and the selected workpack task.
- Works well when there are clear step-by-step procedures 2. Read [device-ecosystem-contract.md](references/device-ecosystem-contract.md) before changing discovery, pairing, identity, presence, routing handoff, or the Gateway device projection.
- Example: DOCX skill with "Workflow Decision Tree" -> "Reading" -> "Creating" -> "Editing" 3. Trace the current producer -> transport -> persistence -> canonical snapshot -> UI path. Confirm the owning service and every caller before editing.
- Structure: ## Overview -> ## Workflow Decision Tree -> ## Step 1 -> ## Step 2... 4. Classify each proposed field and state as one of: discovery candidate, persisted trust relationship, authenticated presence lease, route state, network observation, or derived binding. Reject fields that mix categories.
5. Reuse standard DNS-SD/mDNS for local discovery and the existing request/response control-plane shape for the first protocol slice. Do not treat the current plaintext HTTP transport as sufficient protection for invitations or credentials; require pinned end-to-end identity and confidentiality. Add push transport only when a concrete feature cannot work with bounded polling or heartbeats.
6. Keep persisted relationship mutations schema-versioned, atomic, revision-safe, and reversible. Keep transient session loss from deleting paired identity.
7. Join ecosystem membership to network inventory in the backend and expose one revisioned projection. Never join independent authoritative lists in React. Also use `design-vpn-client-ui` for any visible Connect UI work.
8. For runtime, API, dependency, UI, or deployment changes, also use `manage-harbor-versions` and run its affected-component gate. Skill-only and documentation-only changes need no Harbor version bump.
**2. Task-Based** (best for tool collections) ## Non-negotiable decisions
- Works well when the skill offers different operations/capabilities
- Example: PDF skill with "Quick Start" -> "Merge PDFs" -> "Split PDFs" -> "Extract Text"
- Structure: ## Overview -> ## Quick Start -> ## Task Category 1 -> ## Task Category 2...
**3. Reference/Guidelines** (best for standards or specifications) - Advertise Gateway availability through a versioned DNS-SD service on the local link. Treat every advertisement and resolved address as an untrusted candidate until paired identity is cryptographically verified.
- Works well for brand guidelines, coding standards, or requirements - Put no subscription URL, pairing secret, device list, credential, or private identity material in mDNS/DNS-SD records, logs, fixtures, screenshots, or user-visible diagnostics.
- Example: Brand styling with "Brand Guidelines" -> "Colors" -> "Typography" -> "Features" - Pair explicitly. Use a short-lived, single-use invitation; bind the stable Gateway identity and client installation identity; persist each side atomically only after confirmation; make cross-device finalization idempotent and recoverable; support revoke and credential rotation.
- Structure: ## Overview -> ## Guidelines -> ## Specifications -> ## Usage... - Use a high-entropy QR/manual invitation, or a vetted PAKE plus mutual confirmation for a short human code. Never send a low-entropy code as a bearer secret over unauthenticated HTTP.
- Give every Harbor installation a stable cryptographic device identity. Keep it independent of IP, hostname, interface, and randomized MAC.
- Define states precisely: `discovered` means advertised; `paired` means a persisted trust relationship; `connected` means a current authenticated lease; `routed` means this Gateway is the active route. Never collapse them into one `online` boolean.
- Derive lease freshness from Gateway receipt time. Do not trust a client-supplied clock, IP address, or MAC address as authoritative.
- Correlate a paired client with the existing neighbor inventory only as a derived, confidence-bearing binding. A missing or ambiguous binding must not erase membership or fabricate traffic attribution.
- Allow a client to pair with multiple Gateways, but keep one explicit active routing target. A discovered or unpaired Gateway must never hijack route selection.
- Preserve legacy subscription-based presence only as a bounded transition path. Never silently convert matching subscription URLs into permanent pairing records.
- Keep capability advertisement versioned and additive. Ignore unknown capabilities; block incompatible protocol versions explicitly. Never authorize a mutation from self-declared capabilities alone.
- Keep the Gateway backend as the source of truth for its paired-device registry and active leases. Preserve revision-safe frontend application and transport freshness separation.
**4. Capabilities-Based** (best for integrated systems) ## Acceptance pass
- Works well when the skill provides multiple interrelated features
- Example: Product Management with "Core Capabilities" -> numbered capability list
- Structure: ## Overview -> ## Core Capabilities -> ### 1. Feature -> ### 2. Feature...
Patterns can be mixed and matched as needed. Most skills combine patterns (e.g., start with task-based, add workflow for complex operations). Before handing off an ecosystem change, verify:
Delete this entire "Structuring This Skill" section when done - it's just guidance.] - A supported client can discover more than one Gateway and survives address changes without changing Gateway identity.
- Spoofed discovery cannot create trust, change the active route, or appear as a paired ecosystem member.
- Pairing works without a subscription URL; invitations expire, are single-use, and reject replay.
- The Gateway can distinguish an ordinary LAN neighbor, a paired but disconnected device, a connected Harbor client, and a client currently routed through it.
- A paired Mac remains the same ecosystem device across DHCP, interface, or randomized-MAC changes; only its derived inventory binding changes.
- Discovery loss does not unpair a device; heartbeat loss expires the lease; revocation rejects the next authenticated request immediately.
- Multiple Gateways and multiple clients do not collide. Any aggregate network identity includes Gateway scope.
- Persisted writes are atomic and migrated with rollback; delayed snapshots cannot overwrite newer state.
- Legacy installs retain the documented transition behavior without exposing or copying subscription secrets.
- Automated tests cover discovery deduplication, pairing expiry/replay, authentication, lease expiry, revocation, binding ambiguity, revision ordering, and migration.
- Any visible UI outcome has a separately recorded owner decision and preserves loading, empty, stale, error, keyboard, reduced-motion, and responsive behavior.
## [TODO: Replace with the first main section based on chosen structure] ## Output contract
[TODO: Add content here. See examples in existing skills: Report:
- Code samples for technical skills
- Decision trees for complex workflows
- Concrete examples with realistic user requests
- References to scripts/templates/references as needed]
## Resources (optional) - current owner and traced data flow;
- chosen source of truth for identity, trust, presence, route, and inventory binding;
Create only the resource directories this skill actually needs. Delete this section if no resources are required. - protocol and persistence changes, including version negotiation;
- migration, compatibility, rollback, and security boundaries;
### scripts/ - exact automated checks and what remains unverified;
Executable code (Python/Bash/etc.) that can be run directly to perform specific operations. - unresolved owner decisions, especially visible UI and short-code pairing treatment.
**Examples from other skills:**
- PDF skill: `fill_fillable_fields.py`, `extract_form_field_info.py` - utilities for PDF manipulation
- DOCX skill: `document.py`, `utilities.py` - Python modules for document processing
**Appropriate for:** Python scripts, shell scripts, or any executable code that performs automation, data processing, or specific operations.
**Note:** Scripts may be executed without loading into context, but can still be read by Codex for patching or environment adjustments.
### references/
Documentation and reference material intended to be loaded into context to inform Codex's process and thinking.
**Examples from other skills:**
- Product management: `communication.md`, `context_building.md` - detailed workflow guides
- BigQuery: API reference documentation and query examples
- Finance: Schema documentation, company policies
**Appropriate for:** In-depth documentation, API references, database schemas, comprehensive guides, or any detailed information that Codex should reference while working.
### assets/
Files not intended to be loaded into context, but rather used within the output Codex produces.
**Examples from other skills:**
- Brand styling: PowerPoint template files (.pptx), logo files
- Frontend builder: HTML/React boilerplate project directories
- Typography: Font files (.ttf, .woff2)
**Appropriate for:** Templates, boilerplate code, document templates, images, icons, fonts, or any files meant to be copied or used in the final output.
---
**Not every skill requires all three types of resources.**
@@ -1,4 +1,4 @@
interface: interface:
display_name: "Design Harbor Device Ecosystem" display_name: "Design Harbor Device Ecosystem"
short_description: "Design trusted Harbor device integration." short_description: "Design trusted Harbor device integration."
default_prompt: "Use -harbor-device-ecosystem to design Harbor Gateway discovery, pairing, and connected-device contracts." default_prompt: "Use $design-harbor-device-ecosystem to design Harbor Gateway discovery, pairing, and connected-device contracts."
@@ -0,0 +1,159 @@
# Harbor device ecosystem contract
Use this reference for Gateway advertisement, discovery, pairing, connected-device lists, or integration with future Harbor-capable subsystems.
## Layer ownership
| Layer | Source of truth | Identity | Lifetime | Meaning |
|---|---|---|---|---|
| Discovery | DNS-SD browser cache | advertised Gateway instance | ephemeral | a compatible service may be reachable |
| Gateway trust | client pairing store | `gatewayId` plus pinned public identity | persisted | this is a Gateway the owner paired |
| Peer registry | Gateway pairing store | stable client installation ID plus public identity | persisted | this client belongs to the Harbor ecosystem |
| Presence | Gateway session owner | authenticated client ID plus lease | ephemeral | this paired client is currently connected to the control plane |
| Route | Connect route owner | selected `gatewayId` plus applied route state | runtime/canonical | this client currently intends to use this Gateway |
| Network inventory | existing device inventory | Gateway-scoped MAC-derived `dev_*` ID | observed/persisted | this network endpoint was seen and may own traffic or policy |
| Binding | Gateway backend projection | ecosystem client ID -> network inventory ID | derived | current correlation, never trust identity |
Do not reuse the current MAC-derived `dev_*` ID as an ecosystem ID. For a future fleet view, scope network inventory IDs by `gatewayId`.
## Minimal discovery profile
Advertise one TCP DNS-SD service such as `_harbor-gw._tcp.local.`. Use the service instance name as a short, user-friendly Gateway name; do not encode a MAC address or serial number in it.
Use SRV/A/AAAA for endpoint resolution. Keep TXT metadata small and additive:
- `txtvers=1` for the TXT schema;
- `protovers=1` for the application protocol;
- public `id=<gatewayId>` for candidate deduplication;
- compact capability flags and whether a pairing window is open.
Never advertise credentials, pairing invitations, subscription data, client names, client counts, or private network policy. Unknown TXT keys must be ignored. Resolve addresses immediately before connection and verify the paired identity after connecting.
mDNS is link-local. For another VLAN or routed segment, use an explicitly configured address, QR/manual endpoint, or unicast DNS-SD. Do not invent a custom UDP broadcast or assume multicast crosses routers.
In the current Gateway deployment, first inspect the Compose network boundary: the control service is bridge-networked while the dataplane owns host networking. Keep identity and trust in control, but publish the public DNS-SD descriptor through host Avahi or a narrow host-network publisher. Do not move the whole control plane to host networking merely to gain multicast. On macOS, prefer the native Bonjour/`dns-sd` surface through the existing host-side network monitor over a second custom discovery stack.
Discovery answers only “where might a Gateway be?” It does not answer “do I trust it?”, “am I connected?”, or “is my traffic routed through it?”.
## Identity and pairing
Maintain a stable Gateway identity and a stable key pair in persisted, backup-aware storage. Maintain a stable installation identity and key pair per client; store the client private key in the platform credential store, including macOS Keychain.
Do not reuse the current subscription `hwid` as public ecosystem identity: it is also sent to the subscription provider as `x-hwid`. Create a separate local identity so LAN and provider correlation remain independent.
Model the minimum persisted relationship records:
```text
PairedGateway = gatewayId, displayName, pinnedPublicIdentity,
capabilities, pairedAt
PairedDevice = deviceId, displayName, deviceType, publicIdentity,
capabilities, pairedAt, revokedAt?
```
Treat display names, platform, version, and capabilities as metadata, not authentication or authorization.
Use this pairing sequence:
1. Let the Gateway open a bounded pairing window and create a short-lived, single-use invitation.
2. Bind the invitation to the Gateway ID and public-identity fingerprint.
3. Let the client create or load its installation key and submit its public identity and minimal metadata.
4. Confirm the peer on a trusted surface before committing both records.
5. Issue a per-device credential or register proof-of-possession for later authenticated requests.
6. Consume the invitation atomically; reject expiry, replay, mismatch, or reuse.
7. Support revoke and credential rotation without changing unrelated LAN inventory.
Persist each local trust record atomically and make finalization idempotent so an interrupted client can resume or safely retry. Pairing spans two devices and is not a distributed transaction; never report it as cross-device atomicity.
For the smallest safe first slice, encode the same grouped high-entropy invitation in QR and manual copy/paste form. If the owner requires a short numeric code, use a vetted password-authenticated key exchange such as SPAKE2 and mutual key confirmation, or require an independent confirmation that provides equivalent protection. Use platform or Node standard cryptography for identity and signatures; do not design custom cryptography or add a crypto dependency without need.
Do not use the subscription URL as a pairing credential. Keep the existing subscription-HMAC presence only as an explicitly labelled legacy trust source during the transition window.
## Authenticated presence and route state
After pairing, let the client renew a bounded authenticated lease. Start with periodic requests using the existing control-plane API shape, but protect invitations and credentials with pinned end-to-end identity and confidentiality; the current plaintext HTTP transport alone is not sufficient. Do not add WebSocket, broker, or event-stream infrastructure until a concrete server-push feature requires it.
The Gateway derives:
```text
paired = persisted relationship exists and is not revoked
connected = paired and authenticated lease has not expired
routed = connected and current Connect route state selects this gatewayId
inventory = optional current network binding exists
```
Store or project `lastAuthenticatedAt` and `leaseExpiresAt`; derive status rather than persisting a mutable `online` boolean. Use Gateway receipt time for freshness. Let the client report `deviceType`, app/protocol version, capabilities, and selected route as authenticated metadata, but distinguish a reported route from dataplane-confirmed traffic.
Discovery failure marks the candidate stale. It does not revoke pairing or immediately terminate a still-valid lease. Lease expiry marks the client disconnected without deleting its relationship. Revocation invalidates the credential immediately.
## Joining a Mac to the Gateway device view
When a paired Mac renews its lease:
1. Authenticate its stable ecosystem identity.
2. Derive the remote source address from the accepted connection; do not trust a claimed IP.
3. Correlate that address with the current neighbor snapshot to obtain an optional MAC-derived inventory ID.
4. Publish the relationship, presence, route, and binding together from the Gateway backend.
Represent the binding with `networkDeviceId`, `confidence`, and `observedAt`. A randomized MAC, DHCP change, or interface switch updates or removes only this binding. The stable Mac relationship remains.
If the source address is NATed, missing, duplicated, or maps to an ambiguous neighbor, keep the Mac paired/connected but leave traffic and policy attribution unavailable. Never attach another device's counters by hostname or client-provided MAC.
Expose one revisioned Gateway projection to the UI. It may extend the current device snapshot or use a dedicated ecosystem snapshot, but the backend must perform the join. React must not fetch authoritative pairing, presence, and inventory lists independently and guess the relationship.
## Multi-Gateway behavior
- Let Connect persist several `PairedGateway` records keyed by stable Gateway ID.
- Let discovery resolve zero or more current endpoints for each identity.
- Keep one active route target and make selection or automatic policy explicit.
- Require a paired identity match before automatic handoff. A new advertisement is never enough.
- Preserve a verified active Gateway through transient discovery failure, but surface stale freshness.
- Let one Gateway register many clients without assuming macOS; branch on negotiated capabilities, not hard-coded platform paths.
- Do not add fleet federation. If it becomes real, aggregate with compound `{gatewayId, deviceId}` identities.
## Canonical data and failure rules
- Give the pairing registry its own `schemaVersion` and monotonic `revision`, or include it in an existing canonical aggregate with equivalent guarantees.
- Reuse the existing atomic JSON-store pattern before considering a database or another persistence dependency.
- Make pair, rename, revoke, and credential rotation atomic. Return the complete new snapshot after a mutation.
- Keep active leases process-local unless restart continuity has a demonstrated need. Persist last-seen metadata at a bounded cadence if required; never write every heartbeat by default.
- Apply incoming frontend snapshots only when their revision is not older than the current one. Keep transport stale/error state outside the domain snapshot.
- Preserve last-good discovery and registry data on source errors; expose freshness and the error separately.
- Treat public-key mismatch as an identity error requiring explicit repair or re-pairing, not an address update.
- Treat an unsupported protocol version as an incompatible state, not a generic offline state.
## Compatibility sequence
1. Add the new identity, discovery, pairing, and presence contract without changing current route behavior.
2. Prefer evolving the existing Gateway presence protocol to a version that signs challenges with ecosystem identity instead of creating a second overlapping presence subsystem.
3. Keep the subscription-HMAC default-gateway proof as a labelled `legacy-default-route` source for one documented transition release.
4. Publish the stable `gatewayId` in canonical route state; keep address, UI origin, and freshness as changeable observations.
5. Prefer a paired Gateway when its verified identity matches the current route candidate. Preserve the approved sticky verified-Gateway behavior through transient discovery failure.
6. Never auto-mint a pairing record from a matching subscription URL.
7. When forgetting the active Gateway, demote route state atomically before deleting trust.
8. Preserve existing `dev_*` inventory IDs, aliases, traffic totals, and policies; add only a derived ecosystem binding.
9. Remove the legacy proof only after migration evidence and an explicit cutover task.
## Automated evidence
Cover at least:
- zero, one, and multiple advertisements; deduplication and endpoint changes;
- mDNS loss and recovery without unpairing;
- no secrets in TXT metadata or logs;
- invitation expiry, replay, mismatch, and atomic single-use consumption;
- valid/invalid proof-of-possession, credential rotation, and revocation;
- lease renewal and expiry using Gateway receipt time;
- paired/connected/routed state separation;
- DHCP, interface, and randomized-MAC changes;
- missing and ambiguous inventory binding without false traffic attribution;
- multiple Gateways with one active route;
- revision ordering, migration, rollback, and legacy transition.
## Standards basis
- [RFC 6762: Multicast DNS](https://www.rfc-editor.org/rfc/rfc6762)
- [RFC 6763: DNS-Based Service Discovery](https://www.rfc-editor.org/rfc/rfc6763)
- [RFC 9382: SPAKE2](https://www.rfc-editor.org/rfc/rfc9382)
These standards define discovery and an available PAKE building block. They do not make an mDNS advertisement trustworthy; preserve end-to-end identity verification.
+2 -2
View File
@@ -1,6 +1,6 @@
export const HARBOR_VERSIONS = Object.freeze({ export const HARBOR_VERSIONS = Object.freeze({
macClient: '0.25.10', macClient: '0.25.11',
gatewayClient: '0.26.9', gatewayClient: '0.26.10',
gatewayBackend: '0.26.5', gatewayBackend: '0.26.5',
}); });
+1 -1
View File
@@ -327,7 +327,6 @@ export function DevicesPanel({ feature }: { feature: DevicesFeature }) {
closeLabel="Закрыть устройства" closeLabel="Закрыть устройства"
onClose={onClose} onClose={onClose}
> >
<header className="client-instructions-header client-devices-header">
<div className="client-devices-kicker"> <div className="client-devices-kicker">
<span>Gateway · {devices.length}</span> <span>Gateway · {devices.length}</span>
<span className="client-devices-refresh-wrap client-tooltip-anchor"> <span className="client-devices-refresh-wrap client-tooltip-anchor">
@@ -385,6 +384,7 @@ export function DevicesPanel({ feature }: { feature: DevicesFeature }) {
<Tooltip>Обнулить трафик устройств</Tooltip> <Tooltip>Обнулить трафик устройств</Tooltip>
</span> </span>
</div> </div>
<header className="client-instructions-header client-devices-header">
<h2 id="client-devices-title">Устройства</h2> <h2 id="client-devices-title">Устройства</h2>
<div className="client-instructions-intro"> <div className="client-instructions-intro">
<p>Устройства, которые Gateway видит в локальной таблице соседей.</p> <p>Устройства, которые Gateway видит в локальной таблице соседей.</p>
+10 -2
View File
@@ -15,11 +15,18 @@
} }
.client-devices-kicker { .client-devices-kicker {
position: sticky;
top: 0;
z-index: 30;
width: 100%; width: 100%;
box-sizing: border-box;
display: flex; display: flex;
flex-wrap: nowrap; flex-wrap: nowrap;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
margin-bottom: 9px;
padding: 0 8px;
background: color-mix(in oklch, var(--client-bg) 99%, var(--client-panel));
} }
.client-devices-kicker > span:first-child { .client-devices-kicker > span:first-child {
@@ -232,8 +239,9 @@
animation: client-spin 900ms linear infinite; animation: client-spin 900ms linear infinite;
} }
.client-instructions-header .client-devices-refresh-wrap > .client-tooltip, .client-devices-kicker .client-tooltip {
.client-instructions-header .client-devices-sort-wrap > .client-tooltip { top: calc(100% + 6px);
bottom: auto;
} }
@keyframes client-devices-refresh-progress { @keyframes client-devices-refresh-progress {
+4 -7
View File
@@ -179,6 +179,10 @@
visibility: hidden; visibility: hidden;
} }
.client-secondary-menu:has(.client-rail-action.is-open) .client-rail-action > span {
opacity: 0;
}
.client-drawer { .client-drawer {
position: fixed; position: fixed;
inset: 0 0 0 auto; inset: 0 0 0 auto;
@@ -202,13 +206,6 @@
transition-delay: 0s; transition-delay: 0s;
} }
@media (min-width: 921px) {
.client-drawer {
right: 64px;
width: min(580px, calc(100vw - 64px));
}
}
.client-drawer-sheet { .client-drawer-sheet {
position: relative; position: relative;
min-height: 100%; min-height: 100%;
@@ -205,6 +205,9 @@ test('Gateway device inventory uses the existing accessible responsive drawer',
assert.match(panel, /client-devices-reset-wrap client-tooltip-anchor[\s\S]*aria-label="Обнулить трафик устройств"[\s\S]*<Tooltip>Обнулить трафик устройств<\/Tooltip>/); assert.match(panel, /client-devices-reset-wrap client-tooltip-anchor[\s\S]*aria-label="Обнулить трафик устройств"[\s\S]*<Tooltip>Обнулить трафик устройств<\/Tooltip>/);
assert.doesNotMatch(panel, /className="client-devices-reset"[\s\S]{0,320}<span>/); assert.doesNotMatch(panel, /className="client-devices-reset"[\s\S]{0,320}<span>/);
assert.match(styles, /\.client-devices-kicker \{[^}]*flex-wrap: nowrap/); assert.match(styles, /\.client-devices-kicker \{[^}]*flex-wrap: nowrap/);
assert.match(panel, /<div className="client-devices-kicker">[\s\S]*<header className="client-instructions-header client-devices-header">/);
assert.match(styles, /\.client-devices-kicker \{[^}]*position: sticky;[^}]*top: 0;[^}]*z-index: 30/);
assert.match(styles, /\.client-devices-kicker \.client-tooltip \{[^}]*top: calc\(100% \+ 6px\);[^}]*bottom: auto/);
assert.match(styles, /\.client-devices-reset-wrap \{[^}]*margin-left: auto/); assert.match(styles, /\.client-devices-reset-wrap \{[^}]*margin-left: auto/);
assert.match(styles, /\.client-devices-reset-wrap > \.client-tooltip \{[^}]*white-space: nowrap/); assert.match(styles, /\.client-devices-reset-wrap > \.client-tooltip \{[^}]*white-space: nowrap/);
assert.match(styles, /\.client-devices-reset-wrap\.client-tooltip-anchor:hover > \.client-tooltip,[\s\S]*transform: translate\(0, 0\)/); assert.match(styles, /\.client-devices-reset-wrap\.client-tooltip-anchor:hover > \.client-tooltip,[\s\S]*transform: translate\(0, 0\)/);
+2 -1
View File
@@ -163,11 +163,12 @@ test('secondary menus share one right rail and switch equal drawers as a vertica
assert.match(rule('.client-drawer'), /transform:\s*translateX\(104%\)/); assert.match(rule('.client-drawer'), /transform:\s*translateX\(104%\)/);
assert.match(rule('.client-drawer'), /z-index:\s*50/); assert.match(rule('.client-drawer'), /z-index:\s*50/);
assert.match(rule('.client-drawer'), /box-shadow:/); assert.match(rule('.client-drawer'), /box-shadow:/);
assert.match(styles, /\.client-secondary-menu:has\(\.client-rail-action\.is-open\) \.client-rail-action > span\s*\{[^}]*opacity:\s*0/);
assert.deepEqual( assert.deepEqual(
['.client-confirmation-popup', '.client-secondary-menu', '.client-drawer', '.harbor-versions'].map(zIndex), ['.client-confirmation-popup', '.client-secondary-menu', '.client-drawer', '.harbor-versions'].map(zIndex),
[100, 60, 50, 40], [100, 60, 50, 40],
); );
assert.match(styles, /@media \(min-width: 921px\) \{[\s\S]*\.client-drawer \{[\s\S]*right:\s*64px;[\s\S]*width:\s*min\(580px, calc\(100vw - 64px\)\)/); assert.doesNotMatch(styles, /\.client-drawer\s*\{[^}]*right:\s*64px/);
assert.match(instructions, /<Drawer[\s\S]*className="client-instructions"/); assert.match(instructions, /<Drawer[\s\S]*className="client-instructions"/);
assert.match(routing, /<Drawer[\s\S]*className="client-local-rules"/); assert.match(routing, /<Drawer[\s\S]*className="client-local-rules"/);
assert.match(subscription, /<Drawer[\s\S]*className="client-subscription-drawer"/); assert.match(subscription, /<Drawer[\s\S]*className="client-subscription-drawer"/);
+13 -13
View File
@@ -37,26 +37,26 @@ const expectedImports = [
const sha256 = (value) => crypto.createHash('sha256').update(value).digest('hex'); const sha256 = (value) => crypto.createHash('sha256').update(value).digest('hex');
const acceptedLedger = { const acceptedLedger = {
counts: { counts: {
cascadeEdges: 818, cascadeEdges: 821,
customProperties: 103, customProperties: 103,
declarations: 3300, declarations: 3308,
important: 0, important: 0,
keyframes: 56, keyframes: 56,
media: 13, media: 12,
rules: 947, rules: 947,
variableReferences: 808, variableReferences: 810,
}, },
hashes: { hashes: {
cascadeEdges: 'e5115e193b09067562309034ecfed8dd35d7435562b581036da00190dfd95650', cascadeEdges: '54abf37aa617e90947c3e0cfd35f2358f8873bda512992c5628950d828ea7486',
customProperties: 'ef70fb1d9b61b177f1939f811babe1116bee631de26f5eac0aa0d0009ca5a1fb', customProperties: 'ef70fb1d9b61b177f1939f811babe1116bee631de26f5eac0aa0d0009ca5a1fb',
declarations: '3e6d3c24e0df278909fd0200470193378866819c1a62a6d0404e74297cab45d7', declarations: '97e97ccb50010db1950b2a2ceeb3bdbcc8574aaee5a676724d3332a69fab537d',
duplicateKeyframes: '4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945', duplicateKeyframes: '4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945',
duplicateSelectors: '0fa66e370695261a2a5c6a189752c07d518d80d68d3e79a3ba4bb34407893c0e', duplicateSelectors: '0fa66e370695261a2a5c6a189752c07d518d80d68d3e79a3ba4bb34407893c0e',
keyframes: 'd4378751f36eccc87923c12540315462055032a5d34978a0f45ecada0a5cc1ce', keyframes: 'd4378751f36eccc87923c12540315462055032a5d34978a0f45ecada0a5cc1ce',
ruleDeclarationSequences: 'ef07abd92d371c865e70dc8e709a396ec46056164035594a8cddda6b0f61a629', ruleDeclarationSequences: '14f4eceda4a1811799b7dc50491c9298c27c5796fa8fc8ce02104bdb268d7675',
selectors: '085df6f266f22b99fc429c089b3142f12fd6f74bfb9b86233c55bf8baa92469c', selectors: '1420d737139ad3e03bbdcf5a4e1573443e500cda572b4461bdc578cd6b3c04de',
variableReferences: '664c093241c4076ca05a9dda06ae88d4e7024c926c4636109790201df8212df4', variableReferences: 'c8e01fce4a7f3a976040f20938ec8d70403bd003f66ef13b5637c77f9c4cef08',
witnesses: 'ac771c94e7f2c409c3d317de610c2dae5722eb1d6f0b6626a4e1aa986c83fd94', witnesses: 'b47da99c00da77a5c9716796304fab2163fb21c6bc6753176f008f85a3420695',
}, },
}; };
@@ -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); assert.equal((main.match(/import ['"][^'"]+\.css['"]/g) || []).length, 1);
const assets = fs.readdirSync(path.join(root, 'dist/assets')).filter((file) => file.endsWith('.css')); const assets = fs.readdirSync(path.join(root, 'dist/assets')).filter((file) => file.endsWith('.css'));
assert.deepEqual(assets, ['index-DhqvaTQ4.css']); assert.deepEqual(assets, ['index-0HVoD6xZ.css']);
const built = fs.readFileSync(path.join(root, 'dist/assets', assets[0])); const built = fs.readFileSync(path.join(root, 'dist/assets', assets[0]));
assert.equal(built.byteLength, 127639); assert.equal(built.byteLength, 127874);
assert.equal(sha256(built), 'c5e9ec300671c6660fb23bfaa725a53b165dd08e387362cfac3fbcf709a8d9e9'); assert.equal(sha256(built), '97241721abba48f5a120e08c425fb67b8b3ab560e59b479351ea455837838805');
}); });