diff --git a/changelog/ens-components.mdx b/changelog/ens-components.mdx new file mode 100644 index 0000000..fb6bd97 --- /dev/null +++ b/changelog/ens-components.mdx @@ -0,0 +1,33 @@ +--- +title: "ENS Components" +description: "Changelog for ENS Components releases." +sidebarTitle: "ENS Components" +icon: "puzzle-piece" +--- + + + - Changed: + - Updated avatar and header record support to use `@thenamespace/avatar` v2. + - Externalized the Avatar SDK from the browser bundle. + + + + - Fixed: + - Populated ENS registration fees when the registration form opens. + - Preserved the displayed registration fees when no wallet is connected. + + + + - Fixed: + - Stabilized ENS registration fee estimation. + - Added an RPC fallback for fee estimation. + - Displayed estimated fees with consistent precision. + + + + - Added: + - Calendar-based duration selection for ENS registration and onchain subname minting. + - Agent instructions and quick-start resources for ENS Components integrations. + - Changed: + - Updated duration handling to use seconds across registration and minting flows. + diff --git a/custom.css b/custom.css new file mode 100644 index 0000000..21e5541 --- /dev/null +++ b/custom.css @@ -0,0 +1,477 @@ +/* Snippet styles. Mintlify auto-loads every .css file in the content dir on + every page, so nothing here needs a docs.json entry or an import. + + .fx-* FlowExplainer — SVG CCIP-Read resolution diagram + .rf-* ResolveFlow — earlier station/wire walkthrough */ + + +/* ── ResolveFlow ────────────────────────────────────────────────────────── + Self-contained styles; colors flip on Mintlify's .dark class. */ + +.rf { + --rf-primary: #2b6cb0; + --rf-primary-soft: rgba(43,108,176,.18); + --rf-bg: #fbfcfe; + --rf-panel: #f3f5f9; + --rf-card: #fff; + --rf-border: #dfe3ea; + --rf-line: #c6ccd6; + --rf-text: #12161d; + --rf-text-2: #545c69; + --rf-hint: #858d99; + --rf-warn-bg: rgba(180,120,0,.12); --rf-warn: #8a5b00; + --rf-ok-bg: rgba(0,140,90,.12); --rf-ok: #06724b; + --rf-ease: cubic-bezier(.23,1,.32,1); + /* Breakpoints below key off this box, not the viewport: the docs content + column narrows when the sidebar and TOC appear, so viewport width says + nothing about how much room the diagram actually has. */ + container: rf / inline-size; + font-size: 14px; + border: 1px solid var(--rf-border); + border-radius: 12px; + background: var(--rf-bg); + padding: 18px; + display: flex; flex-direction: column; gap: 16px; + outline: none; +} +.dark .rf, html[data-theme="dark"] .rf { + --rf-primary: #7cb9ff; + --rf-primary-soft: rgba(124,185,255,.2); + --rf-bg: #0c0912; + --rf-panel: #120e1a; + --rf-card: #171223; + --rf-border: #2a2338; + --rf-line: #3a3150; + --rf-text: #f2eefa; + --rf-text-2: #a79fbb; + --rf-hint: #7c7391; + --rf-warn-bg: rgba(255,190,80,.14); --rf-warn: #ffc55c; + --rf-ok-bg: rgba(80,220,160,.14); --rf-ok: #5fe0a8; +} +.rf:focus-visible { outline: 2px solid var(--rf-primary); outline-offset: 4px; } + +.rf-head { font-weight: 700; font-size: 17px; color: var(--rf-text); + display: flex; flex-wrap: wrap; align-items: baseline; gap: .3em; letter-spacing: -.01em; } +.rf-dim { color: var(--rf-text-2); font-weight: 600; } +.rf-arrow { color: var(--rf-primary); } + +/* Stage */ +.rf-stage { display: flex; align-items: center; padding: 14px 10px; + border: 1px solid var(--rf-border); border-radius: 10px; background: var(--rf-panel); overflow: hidden; } +.rf-split { display: flex; flex-direction: column; gap: 8px; flex: 0 0 auto; } + +.rf-st { flex: 0 0 auto; width: 106px; padding: 8px 10px; display: flex; flex-direction: column; gap: 1px; + border: 1px solid var(--rf-border); border-radius: 8px; background: var(--rf-card); + transition: transform .22s var(--rf-ease), border-color .22s var(--rf-ease), + box-shadow .22s var(--rf-ease), opacity .22s var(--rf-ease); } +.rf-st[data-state="off"] { opacity: .42; } +.rf-st[data-state="on"], .rf-st[data-state="pulse"] { + border-color: var(--rf-primary); + box-shadow: 0 0 0 1px var(--rf-primary-soft), 0 6px 24px -12px var(--rf-primary); + transform: translateY(-2px); } +.rf-st[data-state="pulse"] { animation: rf-pulse 1200ms ease-in-out infinite; } +@keyframes rf-pulse { 0%,100% { transform: translateY(-2px) scale(1) } 50% { transform: translateY(-2px) scale(1.035) } } +.rf-chain { font-size: 9px; font-weight: 600; letter-spacing: .07em; text-transform: uppercase; color: var(--rf-hint); } +.rf-st[data-state="on"] .rf-chain, .rf-st[data-state="pulse"] .rf-chain { color: var(--rf-primary); } +.rf-name { font-size: 12px; font-weight: 700; color: var(--rf-text); } +.rf-sub { font-size: 10px; color: var(--rf-text-2); word-break: break-all; } + +/* Connectors */ +.rf-link { flex: 1 1 30px; min-width: 24px; padding: 0 4px; + display: flex; flex-direction: column; justify-content: space-around; gap: 10px; } +.rf-link[data-flow="reverse"] { transform: scaleX(-1); } +.rf-wire { container-type: inline-size; position: relative; height: 2px; border-radius: 2px; background: var(--rf-line); } +.rf-wire::after { content: ""; position: absolute; inset: 0; border-radius: inherit; + background: linear-gradient(90deg, var(--rf-primary-soft), var(--rf-primary)); + transform: scaleX(0); transform-origin: left center; } +.rf-packet { position: absolute; top: 50%; left: 0; width: 7px; height: 7px; margin-top: -3.5px; + border-radius: 50%; background: var(--rf-primary); box-shadow: 0 0 10px 2px var(--rf-primary-soft); + opacity: 0; transform: translateX(-4px); } +.rf-link[data-flow="forward"] .rf-packet, .rf-link[data-flow="reverse"] .rf-packet { + animation: rf-travel 900ms ease-in-out forwards; } +.rf-link[data-flow="forward"] .rf-wire::after, .rf-link[data-flow="reverse"] .rf-wire::after { + animation: rf-trail 900ms ease-in-out forwards; } +.rf-link .rf-wire:nth-child(2) .rf-packet, .rf-link .rf-wire:nth-child(2)::after { animation-delay: 90ms; } +@keyframes rf-travel { + 0% { opacity: 0; transform: translateX(-4px) } + 12% { opacity: 1 } 88% { opacity: 1 } + 100% { opacity: 0; transform: translateX(calc(100cqw - 4px)) } } +@keyframes rf-trail { from { transform: scaleX(0) } to { transform: scaleX(1) } } + +/* Rail */ +.rf-rail { display: flex; gap: 5px; } +.rf-tab { flex: 1 1 0; display: flex; flex-direction: column; gap: 6px; padding: 0; border: 0; + background: none; cursor: pointer; font: inherit; font-size: 10px; font-weight: 600; + text-align: left; color: var(--rf-hint); transition: color .2s var(--rf-ease), transform .16s var(--rf-ease); } +.rf-tab:active { transform: scale(.97); } +.rf-tab[data-state="on"] { color: var(--rf-text); } +.rf-tab[data-state="past"] { color: var(--rf-text-2); } +.rf-bar { height: 3px; border-radius: 3px; background: var(--rf-line); + transform-origin: left center; transition: background-color .24s var(--rf-ease); } +.rf-tab[data-state="past"] .rf-bar, .rf-tab[data-state="on"] .rf-bar { background: var(--rf-primary); } +.rf-tab[data-state="on"] .rf-bar { animation: rf-grow 260ms var(--rf-ease); } +@keyframes rf-grow { from { transform: scaleX(.2) } to { transform: scaleX(1) } } +@media (hover: hover) and (pointer: fine) { .rf-tab:hover { color: var(--rf-text); } } + +/* Body */ +.rf-title { font-size: 15px; font-weight: 700; color: var(--rf-text); + display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin: 0 0 6px; } +.rf-blurb { font-size: 13px; line-height: 1.6; color: var(--rf-text-2); margin: 0; } +.rf-badge { font-size: 10px; font-weight: 600; padding: 3px 8px; border-radius: 999px; } +.rf-badge[data-tone="warn"] { background: var(--rf-warn-bg); color: var(--rf-warn); } +.rf-badge[data-tone="ok"] { background: var(--rf-ok-bg); color: var(--rf-ok); } + +.rf-payload { padding: 12px; border: 1px solid var(--rf-border); border-radius: 10px; + background: var(--rf-card); overflow-x: auto; } +.rf-ptitle { font-size: 10px; font-weight: 600; letter-spacing: .05em; text-transform: uppercase; + color: var(--rf-hint); margin: 0 0 10px; } +.rf-lines { display: flex; flex-direction: column; gap: 7px; margin: 0; } +.rf-line { display: grid; grid-template-columns: 88px minmax(0,1fr); gap: 10px; + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 11.5px; line-height: 1.5; + opacity: 0; transform: translateY(6px); animation: rf-in 320ms var(--rf-ease) forwards; } +.rf-line dt { color: var(--rf-primary); white-space: nowrap; margin: 0; } +.rf-line dd { color: var(--rf-text); word-break: break-all; margin: 0; } +.rf-line[data-soft="true"] dd { color: var(--rf-hint); } +.rf-line:nth-child(1) { animation-delay: 0ms } +.rf-line:nth-child(2) { animation-delay: 45ms } +.rf-line:nth-child(3) { animation-delay: 90ms } +.rf-line:nth-child(4) { animation-delay: 135ms } +.rf-line:nth-child(5) { animation-delay: 180ms } +@keyframes rf-in { to { opacity: 1; transform: translateY(0) } } + +/* Controls */ +.rf-ctls { display: flex; gap: 8px; } +.rf-btn { padding: 7px 14px; border: 1px solid var(--rf-border); border-radius: 999px; + background: var(--rf-card); color: var(--rf-text); font: inherit; font-size: 12px; font-weight: 600; + cursor: pointer; transition: transform .16s var(--rf-ease), border-color .16s var(--rf-ease); } +.rf-btn:active { transform: scale(.97); } +.rf-btn[data-primary] { min-width: 78px; background: var(--rf-primary); border-color: var(--rf-primary); color: var(--rf-bg); } +@media (hover: hover) and (pointer: fine) { .rf-btn:hover { border-color: var(--rf-primary); } } + +/* Narrow: stack the stage, wires travel downward. The row layout needs ~556px; + below that it used to overflow into `overflow: hidden` and lose a station. */ +@container rf (max-width: 570px) { + .rf-stage { flex-direction: column; align-items: stretch; } + .rf-split { flex-direction: row; } + .rf-split > .rf-st { flex: 1 1 0; min-width: 0; } + .rf-st { width: auto; } + .rf-link { min-height: 30px; flex-direction: row; justify-content: center; } + .rf-wire { width: 30px; align-self: center; transform: rotate(90deg); } + .rf-link[data-flow="reverse"] { transform: scaleY(-1); } +} + +/* Only drop the step labels once they genuinely stop fitting. */ +@container rf (max-width: 420px) { + .rf-tab { font-size: 0; gap: 0; } +} + +@media (prefers-reduced-motion: reduce) { + .rf-st[data-state="pulse"] { animation: none; } + .rf-st { transform: none !important; } + .rf-link[data-flow="forward"] .rf-packet, .rf-link[data-flow="reverse"] .rf-packet { + animation: none; opacity: 1; transform: translateX(calc(50cqw - 4px)); } + .rf-link[data-flow="forward"] .rf-wire::after, .rf-link[data-flow="reverse"] .rf-wire::after { + animation: none; transform: scaleX(1); } + .rf-line { animation: rf-in 200ms ease forwards; transform: none; } + .rf-tab[data-state="on"] .rf-bar { animation: none; } +} + +/* ── FlowExplainer ──────────────────────────────────────────────────────── + The palette is self-contained and dark in both site themes: this card owns + its look rather than inheriting the page theme. The extra resets marked + "docs:" undo Mintlify prose styling the diagram never expects. */ + +.fx { + --fx-ink: #0c0f17; + --fx-surface: #151926; + --fx-line: rgba(255, 255, 255, 0.11); + --fx-text: rgba(255, 255, 255, 0.93); + --fx-dim: rgba(255, 255, 255, 0.46); + --fx-faint: rgba(255, 255, 255, 0.24); + --fx-accent: #5aa9ff; + + /* One hue per party, all far enough apart to stay separable when two light + up at once. The accent belongs to motion — edges, chips, packets — so no + node borrows it. */ + --fx-wallet: #7fb2ff; + --fx-eth: #f2a54c; + --fx-gw: #4fc98a; + --fx-chain: #3fd3c6; + --fx-api: #bb8ef7; + + --fx-ease-out: cubic-bezier(0.23, 1, 0.32, 1); + --fx-ease-io: cubic-bezier(0.77, 0, 0.175, 1); + + /* Never set container-type here. Style containment on this element makes + Chrome resolve every `fill: var(--fx-*)` inside the SVG to black — the + custom properties still inherit, but the paint does not. */ + margin: 0; + padding: 22px 22px 16px; + border: 1px solid var(--fx-line); + border-radius: 20px; + background: + radial-gradient(90% 70% at 85% 0%, rgba(90, 169, 255, 0.1), transparent 62%), + radial-gradient(80% 70% at 5% 100%, rgba(187, 142, 247, 0.07), transparent 60%), + var(--fx-ink); + color: var(--fx-text); + font-family: inherit; + box-sizing: border-box; + width: 100%; +} + +/* Toggles: the parent name on the left, where the records live on the right — + the two ends of the diagram they each control. Neither changes the path, + which is the point the card is making. */ +.fx-controls { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + gap: 16px 24px; + margin-bottom: 18px; +} +.fx-toggle { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 7px; +} +.fx-toggle[data-align="end"] { align-items: flex-end; text-align: right; } +.fx-toggle-label { + font-size: 10px; + font-weight: 600; + letter-spacing: 0.09em; + text-transform: uppercase; + color: var(--fx-faint); +} +.fx-toggle-row { + display: flex; + padding: 3px; + border-radius: 999px; + background: rgba(255, 255, 255, 0.04); +} +.fx-btn { + padding: 5px 13px; + border: 0; + border-radius: 999px; + background: transparent; + color: var(--fx-dim); + font-family: inherit; + font-size: 12px; + font-weight: 600; + line-height: 1.4; + cursor: pointer; + transition: color 200ms var(--fx-ease-out), background 200ms var(--fx-ease-out); +} +.fx-btn[data-on="true"] { + background: color-mix(in srgb, var(--fx-accent) 18%, transparent); + color: var(--fx-accent); +} +.fx-btn:focus-visible { outline: 2px solid var(--fx-accent); outline-offset: 3px; } +@media (hover: hover) and (pointer: fine) { + .fx-btn[data-on="false"]:hover { color: var(--fx-text); } +} +.fx-toggle-note { + font-size: 11px; + line-height: 1.4; + color: var(--fx-dim); +} + +.fx-scroll { overflow-x: auto; overscroll-behavior-x: contain; } + +/* The widest docs content column leaves only ~679px inside this box, so a + 720px floor made the diagram scroll at every window size and kept the + right-hand record nodes just off the edge. 640 fits the common desktop + columns outright and still scrolls — visibly, never clipped — once the + column gets genuinely narrow. */ +.fx-svg { display: block; width: 100%; min-width: 640px; height: auto; } + +/* Layer groups */ +.fx-group rect { + fill: rgba(255, 255, 255, 0.018); + stroke: rgba(255, 255, 255, 0.16); + stroke-width: 1; + stroke-dasharray: 4 5; +} +.fx-group text { + fill: var(--fx-faint); + font-size: 10px; + font-weight: 600; + letter-spacing: 0.09em; + text-transform: uppercase; +} + +/* Nodes */ +.fx-node { + transform-box: fill-box; + transform-origin: center; + transition: transform 220ms var(--fx-ease-out); +} +.fx-node rect { + fill: var(--fx-surface); + stroke: var(--fx-line); + stroke-width: 1; + transition: stroke 200ms var(--fx-ease-out), fill 200ms var(--fx-ease-out); +} +.fx-node[data-on="true"] { + transform: scale(1.03); + filter: drop-shadow(0 4px 16px color-mix(in srgb, var(--tone) 26%, transparent)); +} +/* The store this lookup does not use. Present, so the either/or is visible; + dimmed, so it never reads as a second request. */ +.fx-node[data-muted="true"] { opacity: 0.3; } +.fx-node[data-on="true"] rect { + stroke: var(--tone); + fill: color-mix(in srgb, var(--tone) 15%, var(--fx-surface)); +} + +.fx-node[data-tone="wallet"] { --tone: var(--fx-wallet); } +.fx-node[data-tone="eth"] { --tone: var(--fx-eth); } +.fx-node[data-tone="gw"] { --tone: var(--fx-gw); } +.fx-node[data-tone="chain"] { --tone: var(--fx-chain); } +.fx-node[data-tone="api"] { --tone: var(--fx-api); } + +.fx-node-label { + fill: var(--fx-text); + font-size: 13.5px; + font-weight: 600; + letter-spacing: -0.005em; + text-anchor: middle; + transition: fill 200ms var(--fx-ease-out); +} +.fx-node[data-on="true"] .fx-node-label { fill: var(--tone); } +.fx-node-sub { fill: var(--fx-dim); font-size: 10.5px; text-anchor: middle; } + +/* Edges */ +.fx-edge { + fill: none; + stroke: var(--fx-faint); + stroke-width: 1.25; + transition: stroke 220ms var(--fx-ease-out); +} +.fx-edge[data-on="true"] { stroke: var(--fx-accent); } +.fx-arrow { fill: var(--fx-faint); } +.fx-arrow-on { fill: var(--fx-accent); } + +.fx-edge-num { + fill: var(--fx-faint); + font-size: 11px; + font-weight: 600; + text-anchor: middle; + transition: fill 220ms var(--fx-ease-out); +} +.fx-edge-num[data-on="true"] { fill: var(--fx-accent); } + +/* Payload chips — one per active edge, remounted with the beat. */ +.fx-chip { animation: fx-chip 320ms var(--fx-ease-out) both; } +.fx-chip rect { + fill: color-mix(in srgb, var(--fx-accent) 14%, var(--fx-ink)); + stroke: color-mix(in srgb, var(--fx-accent) 38%, transparent); + stroke-width: 1; +} +.fx-chip text { + fill: color-mix(in srgb, var(--fx-accent) 62%, #ffffff); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-size: 10px; + letter-spacing: -0.01em; + text-anchor: middle; +} +@keyframes fx-chip { from { opacity: 0; transform: translateY(3px); } } + +/* Travelling packet — remounted each beat, so it always starts clean. */ +.fx-packet { + fill: var(--fx-accent); + filter: drop-shadow(0 0 6px var(--fx-accent)); + offset-distance: 0%; + opacity: 0; + animation: fx-flow 1500ms var(--fx-ease-io) forwards; +} +@keyframes fx-flow { + 0% { offset-distance: 0%; opacity: 0; } + 10% { opacity: 1; } + 78% { offset-distance: 100%; opacity: 1; } + 100% { offset-distance: 100%; opacity: 0; } +} + +/* Caption + rail */ +.fx-footer { + display: flex; + align-items: center; + gap: 16px; + margin-top: 10px; + padding-top: 12px; + border-top: 1px solid var(--fx-line); +} +.fx-caption { + flex: 1; + display: flex; + align-items: baseline; + gap: 10px; + margin: 0; + min-height: 38px; + font-size: 13.5px; + line-height: 1.45; + color: var(--fx-text); + animation: fx-caption 260ms var(--fx-ease-out); +} +@keyframes fx-caption { from { opacity: 0; transform: translateY(4px); } } + +.fx-step { + flex: 0 0 auto; + display: grid; + place-items: center; + width: 20px; + height: 20px; + border-radius: 50%; + background: color-mix(in srgb, var(--fx-accent) 20%, transparent); + color: var(--fx-accent); + font-size: 11px; + font-weight: 600; +} + +/* Step arrows. Six anonymous dots asked the reader to count; an arrow pair + plus a position readout says where you are and where you can go. */ +.fx-nav { + flex: 0 0 auto; + display: flex; + align-items: center; + gap: 4px; +} +.fx-navbtn { + display: grid; + place-items: center; + width: 30px; + height: 30px; + padding: 0; + border: 1px solid var(--fx-line); + border-radius: 50%; + background: transparent; + color: var(--fx-dim); + font-family: inherit; + font-size: 14px; + line-height: 1; + cursor: pointer; + transition: color 180ms var(--fx-ease-out), border-color 180ms var(--fx-ease-out), + background 180ms var(--fx-ease-out), transform 140ms var(--fx-ease-out); +} +@media (hover: hover) and (pointer: fine) { + .fx-navbtn:hover { + border-color: color-mix(in srgb, var(--fx-accent) 45%, transparent); + background: color-mix(in srgb, var(--fx-accent) 12%, transparent); + color: var(--fx-accent); + } +} +.fx-navbtn:active { transform: scale(0.92); } +.fx-navbtn:focus-visible { outline: 2px solid var(--fx-accent); outline-offset: 3px; } +.fx-count { + min-width: 40px; + text-align: center; + font-size: 11px; + font-variant-numeric: tabular-nums; + color: var(--fx-dim); +} + +@media (prefers-reduced-motion: reduce) { + .fx-packet { display: none; } + .fx-chip, .fx-node, .fx-caption { transition: none; animation: none; transform: none; } + .fx-node[data-on="true"] { transform: none; } +} diff --git a/developer-guide/sdks/ens-components.mdx b/developer-guide/sdks/ens-components.mdx new file mode 100644 index 0000000..9d511d0 --- /dev/null +++ b/developer-guide/sdks/ens-components.mdx @@ -0,0 +1,65 @@ +--- +title: "Overview" +description: "Add ENS workflows to React applications." +sidebarTitle: "Overview" +icon: "puzzle-piece" +--- + +`@thenamespace/ens-components` provides React components for `.eth` registration, ENS record editing, and onchain or offchain subname issuance. The components use wagmi and Viem for wallet and chain interactions. + +## Install + +Install the package and its peer dependencies: + +```bash +npm install @thenamespace/ens-components react react-dom wagmi viem @tanstack/react-query +``` + +Import the component styles once in your application: + +```tsx +import '@thenamespace/ens-components/styles'; +``` + +Your application must provide `WagmiProvider` and `QueryClientProvider`. + +## Use a component + +Import the form that matches your ENS workflow: + +```tsx +import { EnsNameRegistrationForm } from '@thenamespace/ens-components'; + +export function RegisterName() { + return ; +} +``` + +Set `isTestnet` to `true` to use Sepolia instead of Ethereum mainnet. + +## Available components + + + + `EnsNameRegistrationForm` handles the search, commit, wait, and register flow for `.eth` names. + + + `EnsRecordsForm` edits addresses, text records, contenthash, avatar, and header records. + + + `SelectRecordsForm` collects ENS records without requiring a wallet or transaction. + + + `SubnameMintForm` handles price lookup, record selection, and onchain minting from a Namespace listing. + + + `OffchainSubnameForm` creates and updates gasless subnames through the Namespace API. + + + +## Resources + +- Explore the components and their props in the [interactive documentation](https://enscomponents.com/). +- View the source on [GitHub](https://github.com/thenamespace/ens-components). +- Install the [ENS Components skill](/developer-guide/skills#ens-components-skill) for agent-assisted integration. +- Review release changes in the [ENS Components changelog](/changelog/ens-components). diff --git a/developer-guide/sdks/introduction.mdx b/developer-guide/sdks/introduction.mdx index c9ab578..8b614cf 100644 --- a/developer-guide/sdks/introduction.mdx +++ b/developer-guide/sdks/introduction.mdx @@ -21,4 +21,7 @@ SDK packages include: Upload and delete ENS avatar and header images with SIWE authentication. + + Add React interfaces for ENS registration, record editing, and subname issuance. + diff --git a/docs.json b/docs.json index 6a97025..955f840 100644 --- a/docs.json +++ b/docs.json @@ -45,6 +45,7 @@ "overview/for-name-owners", "overview/for-product-teams", "overview/for-fintechs-neobanks", + "overview/make-usernames-universal", "developer-guide/integrations/waas" ] }, @@ -173,6 +174,13 @@ "developer-guide/sdks/avatar/manual-flow", "developer-guide/sdks/avatar/errors" ] + }, + { + "group": "ENS Components", + "icon": "puzzle-piece", + "pages": [ + "developer-guide/sdks/ens-components" + ] } ] }, @@ -241,7 +249,8 @@ "changelog/address-sdk", "changelog/indexer-sdk", "changelog/mint-sdk", - "changelog/avatar-sdk" + "changelog/avatar-sdk", + "changelog/ens-components" ] } ], diff --git a/overview/make-usernames-universal.mdx b/overview/make-usernames-universal.mdx new file mode 100644 index 0000000..0321a49 --- /dev/null +++ b/overview/make-usernames-universal.mdx @@ -0,0 +1,74 @@ +--- +title: "Make your usernames universal" +description: "Give the usernames you already issue an ENS name." +sidebarTitle: "Universal usernames" +icon: "globe" +--- + +import { FlowExplainer } from '/snippets/flow-explainer.jsx' + +Inside your product, `happy.brand` is enough to find someone or send them money. Outside it, the name means nothing. No other wallet has heard of your suffix, so your user goes back to pasting an address. + +The usual fix is to ask every wallet to support your names. That is a long list of integrations you do not control. + +The shorter path is a second name pointing at the same person and the same records: + +``` +Your username: happy.brand +ENS name: happy.brand.eth + or happy.brand.id, using a domain you already own +``` + +Your system stays the source of truth and no data moves. The username just gains a form that ENS-aware wallets already know how to read. + + +This does not turn `.brand` into a new ENS top-level domain. Each user gets an ENS name for the identity they already have. + + +## ENS is not only about .eth + +Two assumptions stop teams before they start. + +**You do not have to use a `.eth` name.** Either register `brand.eth` and issue `happy.brand.eth`, or take a domain you already own like `brand.id`, turn on DNSSEC, and [import it into ENS](https://docs.ens.domains/learn/dns/). Both resolve the same way, so choose on branding, not mechanics. + +**ENS names are not only for Ethereum.** One name holds an address record per chain, and they need not be EVM chains. Bitcoin, Solana, Base, Arbitrum, and Ethereum can all sit on the same name, alongside profile fields like a display name and avatar. + +## Follow one lookup + +The diagram is a single name being looked up, start to finish. Its two toggles are the decisions above: the parent name, and where records are read from. Neither changes the path. + +It loops on its own. Hover to pause, or use the dots to step through it. + + + +Nothing there asked you to move your users, usernames, or records. The wallet asked a question, your system answered, the wallet showed the result. + +## Your records stay where they are + +Records can live in the database you run today, on an L2, or on another chain. No second registry on Ethereum, nothing to keep in sync. Whatever answers the question inside your product answers it here too. + +## Decide before you build + +1. **The naming rule.** A parent name and a predictable pattern, such as `happy.brand` becoming `happy.brand.eth`. Nobody should have to learn a second identity. +2. **Where the answer comes from.** Your database, a contract in your existing registry, or a proof Ethereum can verify. +3. **Who runs it.** Someone holds the parent name, keeps the lookup path online, and fixes it when it breaks. + +## What this does not do + +- Your existing suffix does not become part of ENS. Users gain an ENS name; `.brand` stays yours. +- Support varies. Most major wallets resolve these names, but not every app does, so test the ones your users actually use. +- Anything served this way is public. Addresses, avatars, and profile fields are readable by anyone who asks. + +## Next steps + + + + Compare offchain and onchain names before you pick how to issue them. + + + Create names and manage their records with the Offchain Manager SDK. + + + Bring your existing naming service and we will map it onto ENS with you. + + diff --git a/snippets/flow-explainer.jsx b/snippets/flow-explainer.jsx new file mode 100644 index 0000000..71da840 --- /dev/null +++ b/snippets/flow-explainer.jsx @@ -0,0 +1,386 @@ +/* + * Animated CCIP-Read resolution diagram. + * + * In any .mdx page: + * import { FlowExplainer } from '/snippets/flow-explainer.jsx' + * + * + * Boxed, looping explainer of one lookup, laid out to match the protocol + * diagram: wallet -> ENS resolver -> gateway -> records -> signed response -> + * wallet. Motion is CSS keyframes remounted per beat, so it runs off the main + * thread. + * + * Two toggles, and neither one changes the path: + * + * - Parent name. A `.eth` name and a DNS domain imported into ENS resolve + * identically, so only the string being resolved moves. + * - Records. Step 3 reads from ONE store, never both. On-chain means an RPC + * call to a registry contract; off-chain means an HTTPS call to a system you + * already run. Nothing is fanned out and nothing is merged — the unselected + * store stays on the canvas, muted, so the choice is visible without drawing + * a path nobody takes. + * + * Platform constraints, all forced by Mintlify: + * + * 1. Everything lives INSIDE the exported function. Mintlify compiles snippets + * through MDX and splices only the exported component into the page module, + * dropping module-level consts and helper components. Hoisting NODES/EDGES/ + * BEATS back out turns them into ReferenceErrors at render. + * 2. No types — the MDX pipeline parses JSX, not TSX. + * 3. CSS modules are unavailable, so the styles live as `.fx-*` classes in + * /custom.css. Any .css file in the content dir is auto-loaded site-wide. + * 4. Named export only; hooks are pre-injected, so there is no import line and + * no `React` global. + * 5. Plain
s, never
. Mintlify wraps a
in + * react-medium-image-zoom, which forced two bugs: clicking the diagram + * lifted the bare into a modal, losing the caption and dots, and the + * zoom wrapper is a shrink-to-fit that inherited the svg's min-width + * and pushed the whole page into horizontal scroll on mobile. The + * keeps role="img" and its aria-label, which is what actually carries the + * diagram to a screen reader. + * + * The palette is deliberately self-contained and dark in both site themes, so + * the card reads identically wherever it is dropped. + */ + +export const FlowExplainer = () => { + const BEAT_MS = 2400 + + const ROUTES = [ + { + id: 'ens', + label: '.eth name', + name: 'happy.brand.eth', + note: 'brand.eth registered in ENS', + }, + { + id: 'dns', + label: 'DNS name', + name: 'happy.brand.id', + note: 'brand.id imported into ENS with DNSSEC', + }, + ] + + /* Geometry lives on the store: step 3 draws one path out and step 4 draws + one path back, to whichever store is selected. */ + const STORES = [ + { + id: 'onchain', + label: 'Onchain', + node: 'chain', + note: 'one RPC call to your registry contract', + lookup: 'The gateway makes one RPC call and reads the record from your registry contract.', + back: 'The contract returns the record. Nothing else is queried.', + out: { + d: 'M662,144 C700,140 712,73 750,73', + nx: 706, + ny: 100, + chip: { text: 'eth_call · recordOf(node)', x: 590, y: 112 }, + }, + ret: { + d: 'M750,92 C712,92 700,160 664,164', + nx: 718, + ny: 134, + /* Response lands in the same slot the request left from. */ + chip: { text: 'addr(60) 0x1a9C…4b2f · avatar', x: 600, y: 112 }, + }, + }, + { + id: 'offchain', + label: 'Offchain', + node: 'api', + note: 'one HTTPS request to your API', + lookup: 'The gateway makes one HTTPS request and reads the record from the system you already run.', + back: 'Your API returns the record. Nothing else is queried.', + out: { + d: 'M662,172 C700,176 712,243 750,243', + nx: 706, + ny: 218, + chip: { text: 'GET /records/happy', x: 590, y: 206 }, + }, + ret: { + d: 'M750,224 C712,224 700,180 664,176', + nx: 718, + ny: 196, + chip: { text: 'addr(60) 0x1a9C…4b2f · avatar', x: 600, y: 206 }, + }, + }, + ] + + const [beat, setBeat] = useState(0) + const [paused, setPaused] = useState(false) + const [routeId, setRouteId] = useState('ens') + const [storeId, setStoreId] = useState('onchain') + + const route = ROUTES.find((r) => r.id === routeId) + const store = STORES.find((s) => s.id === storeId) + const NAME = route.name + + const NODES = [ + { id: 'wallet', tone: 'wallet', x: 24, y: 130, w: 116, h: 56, label: 'Wallet / App' }, + { id: 'resolver', tone: 'eth', x: 250, y: 130, w: 160, h: 56, label: 'ENS Resolver', sub: 'CCIP-Read' }, + { id: 'gateway', tone: 'gw', x: 526, y: 130, w: 136, h: 56, label: 'CCIP Gateway' }, + { id: 'chain', tone: 'chain', x: 754, y: 46, w: 176, h: 54, label: 'Onchain records', sub: 'your registry contract' }, + { id: 'api', tone: 'api', x: 754, y: 216, w: 176, h: 54, label: 'Offchain records', sub: 'your API or database' }, + ] + + /* `beat` is the step an edge carries its packet on; `delay` staggers edges + that share one. `chip` is the payload shown while that beat plays. */ + const EDGES = [ + { + id: 'query', + beat: 0, + num: '1', + nx: 193, + ny: 140, + d: 'M140,148 L246,148', + chip: { text: `resolve("${NAME}")`, x: 193, y: 66 }, + }, + { + id: 'ccip', + beat: 1, + num: '2', + nx: 466, + ny: 140, + d: 'M410,148 L522,148', + chip: { text: 'GET /lookup/{sender}/{data}.json', x: 466, y: 84 }, + }, + { id: 'lookup', beat: 2, num: '3', ...store.out }, + { id: 'return', beat: 3, num: '4', ...store.ret }, + { + id: 'signed', + beat: 4, + num: '5', + nx: 466, + ny: 186, + d: 'M522,170 L412,170', + chip: { text: '200 OK · sig 0x1c8f…9d02', x: 466, y: 216 }, + }, + { + id: 'records', + beat: 5, + num: '6', + nx: 194, + ny: 186, + d: 'M246,170 L142,170', + chip: { text: 'addr(60) 0x1a9C…4b2f', x: 194, y: 216 }, + }, + ] + + /* Monospace chip width, estimated from character count. */ + const CHIP_H = 19 + const chipW = (text) => text.length * 5.75 + 16 + + const BEATS = [ + { + active: ['wallet', 'resolver'], + caption: `A wallet asks Ethereum to resolve ${NAME}.`, + }, + { + active: ['resolver', 'gateway'], + caption: + 'The resolver stores no answer. It reverts with a gateway URL — that is CCIP-Read.', + }, + { + active: ['gateway', store.node], + caption: store.lookup, + }, + { + active: [store.node, 'gateway'], + caption: store.back, + }, + { + active: ['gateway', 'resolver'], + caption: 'The gateway signs the response so nothing can be swapped in transit.', + }, + { + active: ['resolver', 'wallet'], + caption: + 'The resolver verifies that signature on Ethereum, and the wallet gets its records.', + }, + ] + + /* Keyed on `beat` as well as `paused`, so stepping with the arrows restarts + the clock instead of leaving a half-spent interval to fire under the + reader — the whole point of a manual step is getting a full beat to look. */ + useEffect(() => { + if (paused) return + const id = setTimeout(() => setBeat((b) => (b + 1) % BEATS.length), BEAT_MS) + return () => clearTimeout(id) + }, [paused, beat]) + + const go = (delta) => setBeat((b) => (b + delta + BEATS.length) % BEATS.length) + + const step = BEATS[beat] + const isOn = (id) => step.active.includes(id) + + return ( +
setPaused(true)} + onMouseLeave={() => setPaused(false)} + > +
+
+ Parent name +
+ {ROUTES.map((r) => ( + + ))} +
+ {route.note} +
+
+ Records +
+ {STORES.map((s) => ( + + ))} +
+ {store.note} +
+
+ +
+ + + + + + + + + + + {/* Every group label sits 8px above its own box, and every box keeps + 26px of air around what it holds. */} + + + Client interface + + Naming layer + + Resolution infrastructure + + + {EDGES.map((e) => ( + + ))} + + {EDGES.filter((e) => e.num).map((e) => ( + + {e.num} + + ))} + + {EDGES.filter((e) => e.beat === beat && e.chip).map((e) => { + const w = chipW(e.chip.text) + return ( + + + {e.chip.text} + + ) + })} + + {EDGES.filter((e) => e.beat === beat).map((e) => ( + + ))} + + {NODES.map((n) => ( + + + + {n.label} + + {n.sub ? ( + + {n.sub} + + ) : null} + + ))} + +
+ +
+

+ {beat + 1} + {step.caption} +

+
+ + + {beat + 1} / {BEATS.length} + + +
+
+
+ ) +} diff --git a/snippets/resolve-flow.jsx b/snippets/resolve-flow.jsx new file mode 100644 index 0000000..b282e58 --- /dev/null +++ b/snippets/resolve-flow.jsx @@ -0,0 +1,281 @@ +/* + * Drop-in Mintlify snippet: animated CCIP-Read walkthrough. + * + * snippets/resolve-flow.jsx + * + * In any .mdx page: + * import { ResolveFlow } from '/snippets/resolve-flow.jsx' + * + * + * Mintlify pre-injects the React hooks, so there is no `React` global and no + * import line here. Named export only — `export default` is not supported. + * + * Everything lives INSIDE the exported function on purpose. Mintlify compiles + * snippets through MDX and splices only the exported component into the page + * module; module-level consts and helper components are dropped. Hoisting the + * data back out reintroduces two failures — the consts become ReferenceErrors, + * and helpers like get rewritten to MDX component lookups that throw + * "Expected component `Station` to be defined". + * + * Self-contained on purpose — no CSS modules, no imports, no design tokens from + * the demo app. Colors are local vars that flip on Mintlify's `.dark` html class. + * Every hex string is the real payload, captured against mainnet resolver + * 0x7974AF8BD3AEe4fe9f8833361fBc3249E3b23aB3 and the live gateway. + * + * Styles live in /custom.css (Mintlify strips