[{"data":1,"prerenderedAt":809},["ShallowReactive",2],{"docs-nav-en":3,"doc-\u002Fen\u002Fdocs\u002Fdesign\u002Frules":80,"docs-search-sections-en":390,"doc-surround-\u002Fen\u002Fdocs\u002Fdesign\u002Frules":805},[4,8,12,16,20,24,28,32,36,40,44,48,52,56,60,64,68,72,76],{"path":5,"title":6,"icon":7},"\u002Fen\u002Fdocs\u002Fquickstart","Quickstart","ri-rocket-line",{"path":9,"title":10,"icon":11},"\u002Fen\u002Fdocs\u002Fdevtool","Local Debugging","ri-terminal-box-line",{"path":13,"title":14,"icon":15},"\u002Fen\u002Fdocs\u002Fatomm","atomm Object","ri-plug-line",{"path":17,"title":18,"icon":19},"\u002Fen\u002Fdocs\u002Fexport","Export","ri-download-2-line",{"path":21,"title":22,"icon":23},"\u002Fen\u002Fdocs\u002Fpublish","Submit for Review & Publish","ri-send-plane-line",{"path":25,"title":26,"icon":27},"\u002Fen\u002Fdocs\u002Ffaq","FAQ","ri-questionnaire-line",{"path":29,"title":30,"icon":31},"\u002Fen\u002Fdocs\u002Fdesign\u002Foverview","Overview","ri-book-open-line",{"path":33,"title":34,"icon":35},"\u002Fen\u002Fdocs\u002Fdesign\u002Fcolor","Colour","ri-palette-line",{"path":37,"title":38,"icon":39},"\u002Fen\u002Fdocs\u002Fdesign\u002Ftypography","Typography","ri-text",{"path":41,"title":42,"icon":43},"\u002Fen\u002Fdocs\u002Fdesign\u002Flayout","Layout","ri-layout-line",{"path":45,"title":46,"icon":47},"\u002Fen\u002Fdocs\u002Fdesign\u002Felevation","Elevation and depth","ri-stack-line",{"path":49,"title":50,"icon":51},"\u002Fen\u002Fdocs\u002Fdesign\u002Fshapes","Shape","ri-shape-line",{"path":53,"title":54,"icon":55},"\u002Fen\u002Fdocs\u002Fdesign\u002Fcomponents","Components","ri-checkbox-multiple-line",{"path":57,"title":58,"icon":59},"\u002Fen\u002Fdocs\u002Fdesign\u002Fmotion","Motion","ri-flashlight-line",{"path":61,"title":62,"icon":63},"\u002Fen\u002Fdocs\u002Fdesign\u002Ficon","Icons","ri-apps-2-line",{"path":65,"title":66,"icon":67},"\u002Fen\u002Fdocs\u002Fdesign\u002Fdirection","Direction (RTL)","ri-global-line",{"path":69,"title":70,"icon":71},"\u002Fen\u002Fdocs\u002Fdesign\u002Faccessibility","Accessibility","ri-eye-line",{"path":73,"title":74,"icon":75},"\u002Fen\u002Fdocs\u002Fdesign\u002Fplatform","Platform constraints","ri-shield-check-line",{"path":77,"title":78,"icon":79},"\u002Fen\u002Fdocs\u002Fdesign\u002Frules","Rule checklist","ri-checkbox-circle-line",{"id":81,"title":78,"body":82,"description":92,"extension":384,"icon":79,"meta":385,"navigation":386,"path":77,"seo":387,"stem":388,"__hash__":389},"docs_en\u002Fen\u002Fdocs\u002Fdesign\u002F12.rules.md",{"type":83,"value":84,"toc":374},"minimark",[85,89,93,97,144,148,186,190,238,242,310,314],[86,87,78],"h1",{"id":88},"rule-checklist",[90,91,92],"p",{},"Gradients, glass, pills, hero glows — the reference point set out in the overview already rules those out. The 26 rules below cover what a reference point cannot carry, and every one of them comes from measuring this system.",[94,95,34],"h3",{"id":96},"colour",[98,99,100,112,122,128,138],"ul",{},[101,102,103,107,108,111],"li",{},[104,105,106],"strong",{},"01","Keep the neutral ramp's cool cast ",[104,109,110],{},"consistent and committed",". Low-chroma greys land in the ambiguous zone and read as dirty.",[101,113,114,117,118,121],{},[104,115,116],{},"02","Make each of the four text steps ",[104,119,120],{},"about half the one above",". Four steps bunched at one end are really two.",[101,123,124,127],{},[104,125,126],{},"03","Recompute the key pairings every time the ramp changes, including the worst case of a small size on a light background.",[101,129,130,133,134,137],{},[104,131,132],{},"04","Keep the board ",[104,135,136],{},"lighter"," than the panels. The artifact is the only thing on screen that should be loud.",[101,139,140,143],{},[104,141,142],{},"05","Judge by measured contrast. At the same ratio, elements that work through area (tracks, bands) and elements that work through line (strokes, ticks) are not equally visible.",[94,145,147],{"id":146},"hierarchy","Hierarchy",[98,149,150,160,170,176],{},[101,151,152,155,156,159],{},[104,153,154],{},"06","Build hierarchy by ",[104,157,158],{},"taking weight away",": let the label fall back to a lighter secondary, and the value steps forward.",[101,161,162,165,166,169],{},[104,163,164],{},"07","Separate three layers within one size using ",[104,167,168],{},"weight plus colour",", rather than adding another size.",[101,171,172,175],{},[104,173,174],{},"08","Give structural labels and helper text their own registers: one is the skeleton, the other is a footnote.",[101,177,178,181,182,185],{},[104,179,180],{},"09","When the hierarchy is unclear, ",[104,183,184],{},"lighten the label"," first — the panel gets lighter overall and the hierarchy gets clearer.",[94,187,189],{"id":188},"shape-and-space","Shape and space",[98,191,192,202,212,222,232],{},[101,193,194,197,198,201],{},[104,195,196],{},"10","Put the uniformity in the ",[104,199,200],{},"shape"," — identical fill, stroke, radius and height.",[101,203,204,207,208,211],{},[104,205,206],{},"11","Let ",[104,209,210],{},"width follow content",", and leave the right edge to the layout's alignment.",[101,213,214,217,218,221],{},[104,215,216],{},"12","Group with ",[104,219,220],{},"dividers"," inside an already-enclosed panel; save cards for things that genuinely leave the plane.",[101,223,224,227,228,231],{},[104,225,226],{},"13","Let elements that never appear together ",[104,229,230],{},"share one slot"," (the unit suffix and the drag handle).",[101,233,234,237],{},[104,235,236],{},"14","Take every value from the 8px scale. A layout that only looks right at 14px has an alignment problem.",[94,239,241],{"id":240},"controls","Controls",[98,243,244,254,264,270,281,295,304],{},[101,245,246,249,250,253],{},[104,247,248],{},"15","Spend the red emphasis step on ",[104,251,252],{},"exactly one"," action per screen, or on none at all.",[101,255,256,259,260,263],{},[104,257,258],{},"16","Treat the state set as ",[104,261,262],{},"closed",". A control that seems to need a ninth state actually needs rethinking.",[101,265,266,269],{},[104,267,268],{},"17","Before rebuilding a native control, write out the keyboard and screen-reader contract you now owe, then repay it item by item.",[101,271,272,275,276,280],{},[104,273,274],{},"18","Put ",[277,278,279],"code",{},"pointer-events: none"," on decorative layers and leave the clicks to the real control underneath.",[101,282,283,286,287,294],{},[104,284,285],{},"19","Give destructive actions the ",[104,288,289,290,293],{},"secondary shell with ",[277,291,292],{},"error"," text"," plus a confirmation step.",[101,296,297,275,300,303],{},[104,298,299],{},"20",[104,301,302],{},"one"," drag affordance on a row, never two.",[101,305,306,309],{},[104,307,308],{},"21","Label buttons with verbs: Generate, Export, Regenerate.",[94,311,313],{"id":312},"engineering","Engineering",[98,315,316,330,340,350,360],{},[101,317,318,321,322,325,326,329],{},[104,319,320],{},"22","Write ",[277,323,324],{},"flex: 1"," on panels that must fill, not just ",[277,327,328],{},"min-height: 0",".",[101,331,332,335,336,339],{},[104,333,334],{},"23","Ship icons as ",[104,337,338],{},"inline stroke SVGs"," and depend on no icon font.",[101,341,342,345,346,349],{},[104,343,344],{},"24","Run a ",[104,347,348],{},"dangling-variable check"," after every token change: deleting a neutral step without updating the semantic token that references it makes some state fail silently.",[101,351,352,355,356,359],{},[104,353,354],{},"25","Use a ",[104,357,358],{},"thin scrollbar"," so a panel's left and right padding stay symmetrical.",[101,361,362,365,366,369,370,373],{},[104,363,364],{},"26","Mount overlays on ",[277,367,368],{},"body"," with ",[277,371,372],{},"position: fixed"," so scroll containers cannot clip them.",{"title":375,"searchDepth":376,"depth":376,"links":377},"",2,[378,380,381,382,383],{"id":96,"depth":379,"text":34},3,{"id":146,"depth":379,"text":147},{"id":188,"depth":379,"text":189},{"id":240,"depth":379,"text":241},{"id":312,"depth":379,"text":313},"md",{},true,{"title":78,"description":92},"en\u002Fdocs\u002Fdesign\u002F12.rules","eMPVm_WlJLB4PGCfaDekDHMiVLuphYH4sJig1gO17YU",[391,395,399,404,407,410,415,420,423,426,431,436,441,446,451,454,457,462,467,472,477,482,487,490,493,498,503,508,513,518,520,523,528,533,538,543,548,553,556,560,563,566,571,576,581,586,591,596,601,604,607,612,617,620,624,629,634,639,644,649,652,656,661,664,668,671,675,680,685,690,695,700,705,710,715,720,723,727,730,734,739,744,747,751,756,759,763,768,773,776,780,782,785,789,793,797,801],{"id":5,"title":6,"titles":392,"content":393,"level":394},[],"Integrate a generator built with any tech stack into Atomm: create a generator → add one line of SDK and integrate capabilities → preview and debug locally in real time → submit for review and publish. No command-line tools required.",1,{"id":396,"title":6,"titles":397,"content":398,"level":394},"\u002Fen\u002Fdocs\u002Fquickstart#quickstart",[],"Integrate a generator built with any tech stack into Atomm: create a generator → add one line of SDK and integrate capabilities → preview and debug locally in real time → submit for review and publish. No command-line tools required. Plain HTML, Vite, Vue, React — any tech stack works. Atomm communicates with your app through a browser-side SDK and doesn't constrain how you build it.",{"id":400,"title":401,"titles":402,"content":403,"level":376},"\u002Fen\u002Fdocs\u002Fquickstart#end-to-end-flow","End-to-end flow",[6],"1. Create a generator in the Developer Console Start by picking a name to serve as its public identifier (must start with a lowercase letter and contain only lowercase letters, digits, and hyphens -; it cannot be changed after creation, as it becomes part of the runtime subdomain and access path). After creation you'll land on the app detail page to continue setup. This name is used again in local preview and final publishing. 2. Add the SDK to your app and integrate capabilities Add one script tag to your page to get the global atomm object: \u003Cscript src=\"https:\u002F\u002Fstatic-res.atomm.com\u002Fscripts\u002Fjs\u002Fgenerator-sdk\u002Fplatform-sdk.js\">\u003C\u002Fscript> Then integrate platform capabilities as needed. Taking export as an example, this takes two steps — place an export button placeholder element (the SDK renders it in place as an Export button), and register an export hook to supply the resulting file: \u003C!-- Place anywhere on the page; the SDK renders it in place as an Export dropdown -->\n\u003Cdiv data-atomm-export-button>\u003C\u002Fdiv> \u002F\u002F The platform calls back into your app for the result file when the user clicks export\natomm.lifecycle.on('export', async () => ({ filename: 'design.glb', blob })) See the atomm object and export capability for details. 3. Start your local server, then preview and verify in real time in the browser Start your local server (e.g. http:\u002F\u002Flocalhost:5173), build the preview URL using the generator name from step 1, and put your local address into ?local= (only loopback addresses such as localhost \u002F 127.0.0.1 are supported): https:\u002F\u002Fwww.atomm.com\u002Fcreativetools\u002Fcommunity\u002Fgenerator\u002F\u003Cgenerator-name>?local=http:\u002F\u002Flocalhost:5173\u002F DevTool runs your local app inside the same environment it will run in live, and previews it there. The integration assistant panel on the right shows whether the SDK is loaded and lists the platform capabilities available to integrate, so you can verify in the preview that export and other capabilities are being invoked correctly. You can also open this directly from the \"Local debugging\" card on the app detail page by entering your local address. See Local debugging for details. 4. Upload the artifact, submit for review, and publish On the app detail page's configuration card, add your listing information, upload the packaged artifact, and submit for review. Once approved, it's automatically published to , and users can open and use it directly from the generator gallery — no installation required. See Submitting for review and publishing for details. html pre.shiki code .sZEs4, html code.shiki .sZEs4{--shiki-default:#E6EDF3}html pre.shiki code .sPWt5, html code.shiki .sPWt5{--shiki-default:#7EE787}html pre.shiki code .sFSAA, html code.shiki .sFSAA{--shiki-default:#79C0FF}html pre.shiki code .s9uIt, html code.shiki .s9uIt{--shiki-default:#A5D6FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sH3jZ, html code.shiki .sH3jZ{--shiki-default:#8B949E}html pre.shiki code .sc3cj, html code.shiki .sc3cj{--shiki-default:#D2A8FF}html pre.shiki code .suJrU, html code.shiki .suJrU{--shiki-default:#FF7B72}",{"id":9,"title":10,"titles":405,"content":406,"level":394},[],"No command-line tools need to be installed. Local debugging takes only two steps: integrate the SDK, then open the online DevTool in your browser to load your local service.",{"id":408,"title":10,"titles":409,"content":406,"level":394},"\u002Fen\u002Fdocs\u002Fdevtool#local-debugging",[],{"id":411,"title":412,"titles":413,"content":414,"level":376},"\u002Fen\u002Fdocs\u002Fdevtool#_1-integrate-the-platform-sdk","1. Integrate the Platform SDK",[10],"Include this in your app page: \u003Cscript src=\"https:\u002F\u002Fstatic-res.atomm.com\u002Fscripts\u002Fjs\u002Fgenerator-sdk\u002Fplatform-sdk.js\">\u003C\u002Fscript> Once loaded, the SDK handshakes with the platform and injects a global atomm object (see atomm object).",{"id":416,"title":417,"titles":418,"content":419,"level":376},"\u002Fen\u002Fdocs\u002Fdevtool#_2-open-the-online-devtool-preview","2. Open the Online DevTool Preview",[10],"After starting your service locally (any port), open the URL below in your browser and fill your local address into ?local= (only loopback addresses such as localhost \u002F 127.0.0.1 are supported): https:\u002F\u002Fwww.atomm.com\u002Fcreativetools\u002Fcommunity\u002Fgenerator\u002F\u003Cgenerator-name>?local=http:\u002F\u002Flocalhost:5173\u002F Left: a live preview of your generator (your local service runs in the same environment it will run in live, with the same capability limits)Right: the integration assistant panel:\nSDK integration hint: shows an \"integration incomplete\" notice until platform-sdk.js is loaded, along with copyable integration code and a documentation link; it disappears automatically once the SDK handshake succeeds.Platform capability list: lists the capabilities the platform provides (export \u002F download, the atomm object, etc.); each item lets you \"view docs\" or \"copy the integration prompt\" to hand off to an AI for integration. Dev preview mode is fixed to the dev environment, incurs no charges, has all platform capabilities enabled by default, and does not depend on any backend data. On the app detail page in the Developer Console, fill in your local address in the \"Local Debugging\" card to open the preview above directly. www.atomm.com runs over HTTPS. Browsers grant a mixed-content exemption for http addresses on localhost, so a local http service loads normally; non-localhost http addresses will be blocked by the browser — use localhost or enable HTTPS for your local service. html pre.shiki code .sZEs4, html code.shiki .sZEs4{--shiki-default:#E6EDF3}html pre.shiki code .sPWt5, html code.shiki .sPWt5{--shiki-default:#7EE787}html pre.shiki code .sFSAA, html code.shiki .sFSAA{--shiki-default:#79C0FF}html pre.shiki code .s9uIt, html code.shiki .s9uIt{--shiki-default:#A5D6FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"id":13,"title":14,"titles":421,"content":422,"level":394},[],"atomm is a global object injected into the page once the platform SDK (platform-sdk.js) loads. It exposes platform capabilities and UI utilities, and can be used directly in your generator code.",{"id":424,"title":14,"titles":425,"content":422,"level":394},"\u002Fen\u002Fdocs\u002Fatomm#atomm-object",[],{"id":427,"title":428,"titles":429,"content":430,"level":376},"\u002Fen\u002Fdocs\u002Fatomm#lifecycle-lifecycle","Lifecycle lifecycle",[14],"Register platform lifecycle hooks via lifecycle.on: atomm.lifecycle.on('export', async () => { ... })",{"id":432,"title":433,"titles":434,"content":435,"level":376},"\u002Fen\u002Fdocs\u002Fatomm#ui-tools-ui","UI Tools ui",[14],"Shows a platform-wide toast notification. Takes an object as its argument: type: optional, one of success \u002F warning \u002F error \u002F info, defaults to successmessage: the notification textduration: optional, how long the toast stays visible, in seconds; 0 means it won't auto-close; defaults to 3 seconds if omitted toast() returns the id of that toast; call closeToast(id) to close it manually. A persistent toast with duration: 0 must be closed this way, or it will stay on screen indefinitely. atomm.ui.toast({ type: 'success', message: '已完成' })\n\n\u002F\u002F Show for 5 seconds\natomm.ui.toast({ type: 'info', message: '正在处理…', duration: 5 })\n\n\u002F\u002F duration: 0 keeps it on screen; close it with closeToast once the task finishes\nconst id = await atomm.ui.toast({ type: 'info', message: '导出中…', duration: 0 })\n\u002F\u002F …task finishes…\nawait atomm.ui.closeToast(id) Passing a string (e.g. toast('已完成')) won't work — the platform only reads message and type off an object, so a string is treated as an empty message. The ui namespace currently provides toast (which returns the toast's id) and closeToast.",{"id":437,"title":438,"titles":439,"content":440,"level":376},"\u002Fen\u002Fdocs\u002Fatomm#app-info-app","App Info app",[14],"\u002F\u002F Current locale code (zh \u002F en \u002F ja… 17 in total)\nconst locale = await atomm.app.getLocale()\n\n\u002F\u002F All supported locales (code + display name), for building a language selector consistent with the platform\nconst locales = await atomm.app.getSupportedLocales()\n\u002F\u002F → [{ code: 'zh', name: '简体中文' }, { code: 'en', name: 'English' }, …]",{"id":442,"title":443,"titles":444,"content":445,"level":376},"\u002Fen\u002Fdocs\u002Fatomm#user-user","User user",[14],"\u002F\u002F Whether the user is currently logged in (returns only a boolean, no token or profile data)\nconst loggedIn = await atomm.user.isLoggedIn()\n\n\u002F\u002F If not logged in, opens the login dialog and waits for the user to finish; returns whether they ended up logged in\nif (!loggedIn) {\n  const ok = await atomm.user.login()\n  if (!ok) return \u002F\u002F User canceled or login failed\n}\n\u002F\u002F Reaching here means the user is logged in — continue with your logic login() opens a login window, so call it from within a user gesture handler (e.g. a click callback) to avoid having it blocked by the browser's popup blocker. Logging in is a long-running interaction, so the platform already extends its timeout for it (about 5 minutes) — you don't need to add your own.",{"id":447,"title":448,"titles":449,"content":450,"level":376},"\u002Fen\u002Fdocs\u002Fatomm#safe-invocation","Safe Invocation",[14],"If you're not sure whether your code is running inside the platform, use optional chaining to avoid errors: window.atomm?.ui?.toast?.({ type: 'info', message: 'hi' }) html pre.shiki code .sZEs4, html code.shiki .sZEs4{--shiki-default:#E6EDF3}html pre.shiki code .sc3cj, html code.shiki .sc3cj{--shiki-default:#D2A8FF}html pre.shiki code .s9uIt, html code.shiki .s9uIt{--shiki-default:#A5D6FF}html pre.shiki code .suJrU, html code.shiki .suJrU{--shiki-default:#FF7B72}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sH3jZ, html code.shiki .sH3jZ{--shiki-default:#8B949E}html pre.shiki code .sFSAA, html code.shiki .sFSAA{--shiki-default:#79C0FF}",{"id":17,"title":18,"titles":452,"content":453,"level":394},[],"Register the export lifecycle hook, and the platform will invoke your generator to retrieve the resulting file whenever the user clicks \"Export\" or \"Open in Studio.\" The return value is always { filename, blob }, and any file format is supported (images, SVG, 3D models, PDF, etc.).",{"id":455,"title":18,"titles":456,"content":453,"level":394},"\u002Fen\u002Fdocs\u002Fexport#export",[],{"id":458,"title":459,"titles":460,"content":461,"level":376},"\u002Fen\u002Fdocs\u002Fexport#placing-the-export-button","Placing the Export Button",[18],"You place the export button yourself, inside your app—add an element with the data-atomm-export-button attribute anywhere on the page, and the SDK will render it in place as a platform-controlled Export dropdown (Download \u002F Open in Studio + credit indicator). Clicks are always routed through the platform: \u003Cdiv data-atomm-export-button>\u003C\u002Fdiv> Position and outer layout are entirely up to you (e.g., position: fixed to pin it to a corner); you can place multiple instances on one page, each hydrating independently.The button renders inside a Shadow DOM for style isolation; theming is done exclusively through the documented --atomm-export-* CSS variables (set them on the element itself or any ancestor—they're automatically inherited by the button): [data-atomm-export-button] {\n  --atomm-export-bg: #d32f2f; \u002F* Button background color *\u002F\n  --atomm-export-radius: 0; \u002F* Border radius *\u002F\n  --atomm-export-width: 200px; \u002F* Width: px, or 100% to fill the container *\u002F\n  --atomm-export-height: 40px; \u002F* Height *\u002F\n} VariableDefaultControls--atomm-export-bg \u002F -bg-hover \u002F -bg-active#070b10 \u002F #252c36 \u002F #1c2129Button background for the default \u002F hover \u002F active states--atomm-export-color#fffButton text color--atomm-export-widthauto (fits content)Button width (px, or 100% to fill the container; min 24px)--atomm-export-height32pxButton height (px or 100%; min 24px)--atomm-export-radius8pxBorder radius--atomm-export-font \u002F -font-sizeInter, system-ui, sans-serif \u002F 14pxFont family \u002F font size--atomm-export-menu-bg \u002F -menu-color \u002F -menu-hover-bg#fff \u002F #111 \u002F #f4f4f5Dropdown menu colors--atomm-export-menu-radius \u002F -menu-shadow8px \u002F 0 4px 16px rgba(16,24,40,.12)Menu border radius \u002F shadow--atomm-export-z1000Dropdown overlay z-index Only the --atomm-export-* variables listed above are supported customization points; internal button class names may change at any time, so don't rely on them. The export button—and its Download \u002F Open in Studio \u002F credit indicator—is driven by the atomm platform, and only works when your generator runs inside the atomm environment: the live platform, or the dev tool's local preview (?local=).If you open your generator standalone, outside atomm (e.g. opening the HTML directly), the button may still render, but clicking it won't produce a file and no billing status will show—this is by design, not a bug. Always verify export via local preview or the live platform.",{"id":463,"title":464,"titles":465,"content":466,"level":379},"\u002Fen\u002Fdocs\u002Fexport#free-use-count-credit-indicator-on-the-button","Free-Use Count \u002F Credit Indicator on the Button",[18,459],"The button automatically displays the current billing status (pushed by the platform—no action needed on your part): StateDisplayMeaningFree window30s countdownA short grace period granted after a successful export; exports within this window are freeFree countFree 3\u002F3Remaining \u002F total free exportsCredit costCredit icon + numberCredits consumed per export once free uses are exhausted In local debugging (?local=), clicking export never actually deducts credits; refreshing the page resets it.",{"id":468,"title":469,"titles":470,"content":471,"level":376},"\u002Fen\u002Fdocs\u002Fexport#registering-the-export-hook","Registering the export Hook",[18],"atomm.lifecycle.on('export', async () => {\n  \u002F\u002F No arguments are passed when triggered; the handler reads the current generation result itself and produces a Blob\n  const blob = await exportCurrentResultAsBlob()\n  if (!blob) {\n    throw new Error('请先生成作品后再下载')\n  }\n\n  return {\n    filename: 'my-generator.glb', \u002F\u002F Must include an extension; this determines the downloaded filename and the asset type used by Open in Studio\n    blob, \u002F\u002F A Blob of any format; MIME type comes from blob.type\n  }\n})",{"id":473,"title":474,"titles":475,"content":476,"level":376},"\u002Fen\u002Fdocs\u002Fexport#return-value-fields","Return Value Fields",[18],"FieldTypeDescriptionfilenamestringThe downloaded filename; must include an extension (e.g., design.glb) and must not contain path separators \u002F \\blobBlobFile content of any format; each file must be ≤ 100MB; MIME type comes from blob.type, falling back to the filename extension when empty",{"id":478,"title":479,"titles":480,"content":481,"level":376},"\u002Fen\u002Fdocs\u002Fexport#multi-file-export-optional","Multi-File Export (Optional)",[18],"The handler can also return an array of files, and the platform will bundle them into a single zip download: atomm.lifecycle.on('export', async () => [\n  { filename: 'model.glb', blob: glbBlob },\n  { filename: 'preview.png', blob: pngBlob },\n]) RuleDescriptionzip filenameDerived from the base name of the first file in the array (model.glb → model.zip)Duplicate namesAutomatically deduplicated to name (1).extSize limitThe combined size of all files must be ≤ 100MB; exceeding this fails the entire exportArray of length 1Downloads the file directly, without wrapping it in a zipOpen in StudioSupports multiple files—all returned files are handed to Studio to open, with no format restriction; formats Studio doesn't support are flagged by Studio itself. Multi-file import requires xTool Studio 1.8+; single-file export remains compatible with older versions Single files still use { filename, blob }; use an array for multiple files. If any item in the returned array is invalid (not a Blob, filename missing an extension, etc.), the entire result is considered invalid.",{"id":483,"title":484,"titles":485,"content":486,"level":376},"\u002Fen\u002Fdocs\u002Fexport#getting-a-blob","Getting a Blob",[18],"Almost any export source can be converted to a Blob in a single line: What you haveConvert to BlobCanvas (2D drawing)canvas.toBlob(cb, 'image\u002Fpng')SVG string \u002F text \u002F JSONnew Blob([str], { type: 'image\u002Fsvg+xml' })ArrayBuffer \u002F 3D mesh (glb\u002Fstl, etc.)new Blob([buffer], { type: 'model\u002Fgltf-binary' })Existing data URL \u002F remote URLawait (await fetch(x)).blob()File from \u003Cinput type=file>Use it directly (a File is already a Blob) Export data is always carried as a Blob, so any format is supported — a Blob preserves the binary content of any file type.filename must include an extension and must not contain path separators; if a file exceeds 100MB or the return value isn't a valid { filename, blob }, the platform treats it as invalid, the export fails, and the user is notified. Downloads support saving any format to disk. \"Open in Studio\" hands the file off to xTool Studio to open—the platform makes no assumptions about format, and Studio itself will flag any format it doesn't support. The export button is available to every generator (it appears as soon as you add data-atomm-export-button to your app). If a user clicks export and you haven't registered the export hook, the platform receives \"the app provides no download method\"—and your submission will be rejected in review as a result. Any generator that supports export must register this hook; you can test it locally by clicking \"Export\" in the preview to confirm a file is produced correctly. html pre.shiki code .sZEs4, html code.shiki .sZEs4{--shiki-default:#E6EDF3}html pre.shiki code .sPWt5, html code.shiki .sPWt5{--shiki-default:#7EE787}html pre.shiki code .sFSAA, html code.shiki .sFSAA{--shiki-default:#79C0FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sQhOw, html code.shiki .sQhOw{--shiki-default:#FFA657}html pre.shiki code .sH3jZ, html code.shiki .sH3jZ{--shiki-default:#8B949E}html pre.shiki code .suJrU, html code.shiki .suJrU{--shiki-default:#FF7B72}html pre.shiki code .sc3cj, html code.shiki .sc3cj{--shiki-default:#D2A8FF}html pre.shiki code .s9uIt, html code.shiki .s9uIt{--shiki-default:#A5D6FF}",{"id":21,"title":22,"titles":488,"content":489,"level":394},[],"Once you've integrated the platform capabilities and verified everything in local preview, you're ready to publish from the Developer Console. There's no local packaging tool needed anymore — just produce static files using your own build process.",{"id":491,"title":22,"titles":492,"content":489,"level":394},"\u002Fen\u002Fdocs\u002Fpublish#submit-for-review-publish",[],{"id":494,"title":495,"titles":496,"content":497,"level":376},"\u002Fen\u002Fdocs\u002Fpublish#_1-prepare-the-artifact","1. Prepare the Artifact",[22],"Build a static Web bundle that's ready to deploy (with the platform SDK already included in the page), then package it into a single .zip file (50 MB max). Your app depends on the platform-sdk.js script tag in the page to connect to platform capabilities in production. Some build configurations can cause the production entry page to drop this script, which results in \"works fine in local preview, but export stops working after publishing.\" Before uploading, make sure the entry page in your production artifact is the same one you verified locally — and that it still includes the SDK.",{"id":499,"title":500,"titles":501,"content":502,"level":376},"\u002Fen\u002Fdocs\u002Fpublish#_2-create-the-application","2. Create the Application",[22],"Click \"Create Application\" in the Developer Console, and give your generator a name to serve as its runtime identifier (must start with a lowercase letter, and contain only lowercase letters, digits, and hyphens). This name is tied to the production subdomain and access path, and cannot be changed after creation — choose carefully. The card title shown to users is configured separately in the next step.",{"id":504,"title":505,"titles":506,"content":507,"level":376},"\u002Fen\u002Fdocs\u002Fpublish#_3-configure-listing-details","3. Configure Listing Details",[22],"Go to \"Application List Details\" on the app detail page, and fill in the card title, short description, and cover image. A live preview of the app card is shown on the right. This information determines how users see your app on the Creative Tools page, and any changes here also require review.",{"id":509,"title":510,"titles":511,"content":512,"level":376},"\u002Fen\u002Fdocs\u002Fpublish#_4-upload-the-code-artifact","4. Upload the Code Artifact",[22],"Switch to \"Code Artifact\" and upload your packaged .zip (containing index.html and other static files). For live local debugging during development, use the online DevTool — see Local Debugging for details.",{"id":514,"title":515,"titles":516,"content":517,"level":376},"\u002Fen\u002Fdocs\u002Fpublish#_5-submit-for-review","5. Submit for Review",[22],"In \"Submit for Review,\" the platform checks that both the listing details and the code artifact are complete. Once everything is ready, click \"Submit for Review\" — the listing changes and the new artifact are bundled into a single \"pending change\" and submitted together. The review result (approved \u002F rejected, with reasons) is sent back to your console via a message. Once submitted, the application enters \"Under Review.\" You'll need to wait for the result before submitting new changes. Once approved, the application is published automatically, and its runtime URL becomes  — users can open it directly from the generator gallery.",{"id":25,"title":26,"titles":519,"content":375,"level":394},[],{"id":521,"title":26,"titles":522,"content":375,"level":394},"\u002Fen\u002Fdocs\u002Ffaq#faq",[],{"id":524,"title":525,"titles":526,"content":527,"level":376},"\u002Fen\u002Fdocs\u002Ffaq#do-i-need-to-install-a-command-line-tool","Do I need to install a command-line tool?",[26],"No. Integration only requires adding a single line to import platform-sdk.js into your page. For debugging, just open the online DevTool in your browser and load your local service.",{"id":529,"title":530,"titles":531,"content":532,"level":376},"\u002Fen\u002Fdocs\u002Ffaq#which-tech-stacks-are-supported","Which tech stacks are supported?",[26],"Any of them. Plain HTML, Vite, Vue, React — anything that ultimately produces deployable static web files will work.",{"id":534,"title":535,"titles":536,"content":537,"level":376},"\u002Fen\u002Fdocs\u002Ffaq#what-address-should-my-local-service-use-why-wont-my-address-load","What address should my local service use? Why won't my address load?",[26],"Preview loads your local service via ?local= (only loopback addresses such as localhost \u002F 127.0.0.1 are supported). www.atomm.com runs over https, and browsers only allow http for localhost; http addresses other than localhost are blocked by the mixed-content policy. Use localhost, or enable https for your local service.",{"id":539,"title":540,"titles":541,"content":542,"level":376},"\u002Fen\u002Fdocs\u002Ffaq#platform-capabilities-arent-available-or-the-panel-shows-integration-incomplete-what-should-i-do","Platform capabilities aren't available, or the panel shows \"Integration incomplete\" — what should I do?",[26],"This means the page failed to load platform-sdk.js. Check that the script is included correctly and that your local service is reachable, then reload the page. Once the SDK handshake succeeds, the message will clear automatically.",{"id":544,"title":545,"titles":546,"content":547,"level":376},"\u002Fen\u002Fdocs\u002Ffaq#can-the-generator-name-runtime-identifier-be-changed","Can the generator name (runtime identifier) be changed?",[26],"No. The name becomes part of the runtime subdomain and access path and is tied to your live URL. It cannot be changed after creation, so choose it carefully.",{"id":549,"title":550,"titles":551,"content":552,"level":376},"\u002Fen\u002Fdocs\u002Ffaq#what-happens-if-i-dont-integrate-export-functionality","What happens if I don't integrate export functionality?",[26],"The export button is available to every generator (it appears wherever you place data-atomm-export-button in your app). If you haven't registered the export hook, users who click export will get \"The app does not provide a download method,\" and this will get your submission rejected during review. To support export, you must register the export hook — you can test it in local preview by clicking \"Export\" to confirm it produces a file correctly.",{"id":29,"title":30,"titles":554,"content":555,"level":394},[],"These guidelines come in two forms: the pages below are written for people, and design.md is written for coding agents — the same tokens, component specs and design intent as a single file in the standard DESIGN.md format. Hand it straight to yours.",{"id":557,"title":30,"titles":558,"content":559,"level":394},"\u002Fen\u002Fdocs\u002Fdesign\u002Foverview#overview",[],"These guidelines come in two forms: the pages below are written for people, and design.md is written for coding agents — the same tokens, component specs and design intent as a single file in the standard DESIGN.md format. Hand it straight to yours. Download design.md These are recommendations, not a review gate. Follow them and your generator reads as part of the platform rather than another website embedded inside it — nobody has to relearn what a control looks like or what happens when they click it. Ignore them and you can still publish; visual style does not decide the review. The only chapter that is enforced is Platform constraints: export goes through the platform button, your generator gets only the region the platform allocates to it, the exported file is 1:1 actual size. Those are not matters of taste but of whether the thing runs and whether it passes review — a submission with no export hook registered is rejected automatically. Everything else below is advice. A generator is a workbench, not a storefront. The reference point is the properties panel of a professional creative tool — the chrome is monochrome and unremarkable, so the thing being made is the only coloured object on screen. Whoever is using this is in the middle of working. They will run the same generator twenty times in an afternoon, changing one value each time. At that repetition rate, the value of the interface is its predictability: controls stay in the same place, values stay legible, and the twentieth transition looks exactly like the first. The tone is quiet and transparent. Hierarchy is built by taking weight away: labels fall back to a lighter secondary, and only values, the primary button and the artifact itself are dark. One stroke of red is held in reserve, marking the single action on screen with the heaviest consequences. Board14 \u002F 500 · primary text\n      Width\n        mm\n      Columns\n      Randomness\n        %\n      30 pieces, 33.33 × 30.00 mm each.\n    Field label 14 \u002F 400 \u002F 7.56:1 → value 14 \u002F 500 \u002F 17.74:1. What separates them is one step of weight plus one step of colour — not size. The label steps back, the value steps forward; the same 14px still has room for a section header (500 \u002F primary) and helper text (12px \u002F tertiary). When the hierarchy is unclear, lighten the label rather than heavying it — the panel as a whole gets lighter, not louder. What it is not: not a marketing page, not a dashboard, not a consumer app. No hero moment, no gradients, no glass, no glow, no pill buttons, no illustrated empty state apologising for itself, and no entrance animation on load. Those belong to products that still have to convince you to stay. This one already has your attention.",{"id":33,"title":34,"titles":561,"content":562,"level":394},[],"Written in two layers. Primitives hold the raw values and are the only place a hex appears; semantic tokens reference them and say where a colour is used. Components reference the semantic layer, never a primitive.",{"id":564,"title":34,"titles":565,"content":562,"level":394},"\u002Fen\u002Fdocs\u002Fdesign\u002Fcolor#colour",[],{"id":567,"title":568,"titles":569,"content":570,"level":376},"\u002Fen\u002Fdocs\u002Fdesign\u002Fcolor#neutral-ramp","Neutral ramp",[34],"Thirteen steps. Click a swatch to copy its hex. neutral-0\n       #ffffff\n     \n   \n     \n     \n       neutral-25\n       #fbfcfd\n     \n   \n     \n     \n       neutral-50\n       #f9fafb\n     \n   \n     \n     \n       neutral-100\n       #f3f4f6\n     \n   \n     \n     \n       neutral-200\n       #e5e7eb\n     \n   \n     \n     \n       neutral-300\n       #d1d5db\n     \n   \n     \n     \n       neutral-400\n       #9ca3af\n     \n   \n     \n     \n       neutral-500\n       #6b7280\n     \n   \n     \n     \n       neutral-600\n       #4b5563\n     \n   \n     \n     \n       neutral-700\n       #374151\n     \n   \n     \n     \n       neutral-800\n       #1f2937\n     \n   \n     \n     \n       neutral-850\n       #18202f\n     \n   \n     \n     \n       neutral-900\n       #111827\n     \n   How the semantic layer maps back: 900 → brand \u002F values · 600 → field labels · 500 → helper text · 400 → disabled \u002F ruler numbers · 300 → input strokes \u002F slider track · 200 → overlay strokes · 100 → segmented track · 25 → board",{"id":572,"title":573,"titles":574,"content":575,"level":376},"\u002Fen\u002Fdocs\u002Fdesign\u002Fcolor#a-consistent-cool-cast-reads-as-clean-leftover-chroma-reads-as-dirty","A consistent cool cast reads as clean; leftover chroma reads as dirty",[34],"Every step of this ramp leans cool, with an RGB spread reaching 19–26 through the mid-tones. That is deliberate, not a ramp someone forgot to neutralise. 36101921242422The cool cast is consistent and committed: every step shares a hue direction, with an RGB spread of 19–26 through the mid-tones. That is deliberate, not a ramp someone forgot to neutralise. Low-chroma greys land in the ambiguous zone — not neutral enough to read as grey, not cool enough to read as cool grey. Be fully neutral or be decisively cool. The one place pure neutral grey is used is the ruler ticks: a uniformly cool interface with neutral instrument markings, each coherent on its own terms. The one exception is the ruler ticks, which use pure neutral grey. A uniformly cool interface and neutral instrument markings each hold up on their own terms without interfering.",{"id":577,"title":578,"titles":579,"content":580,"level":376},"\u002Fen\u002Fdocs\u002Fdesign\u002Fcolor#four-text-steps-each-roughly-half-the-contrast-of-the-one-above","Four text steps, each roughly half the contrast of the one above",[34],"text-primary→ neutral-900\n     Value 200 mm · section header · body\n     17.74:1\n   \n     text-secondary→ neutral-600\n     Field labels · secondary buttons · icon buttons\n     7.56:1\n   \n     text-tertiary→ neutral-500\n     Helper text · unit suffixes · timestamps\n     4.83:1\n   \n     text-disabled→ neutral-400\n     Disabled · ruler numbers\n     2.54:1\n   Spread the four steps evenly, each about half the one above. The eye reads ratios, not absolutes: 17.7 and 12.4 land as the same step, while the jump from 4.8 to 2.5 is unmistakable. Only with four evenly spaced steps does \"make the label step back\" have somewhere to land.",{"id":582,"title":583,"titles":584,"content":585,"level":376},"\u002Fen\u002Fdocs\u002Fdesign\u002Fcolor#three-stroke-steps-each-with-a-clear-job","Three stroke steps, each with a clear job",[34],"stroke-divider\n        Between items on the same surface: between sections, under a panel header, between stat blocks. The weakest step.\n    \n    \n      \n      stroke-default\n        The boundary of overlays and cards: select popovers, canvas overlays, tooltips, the outline of the sheet. A shadow helps shape it too.\n    \n    \n      \n      stroke-input\n        Inputs, value pills, select triggers, secondary buttons.The boundary of an interactive control is one step heavier than a decorative one.\n    \n  Signal hover with a heavier stroke, not a lighter fill. surface-subtle and background-input sit very close together in light mode, so changing the fill often changes nothing visible; lifting the stroke from stroke-input to stroke-strong always shows.",{"id":587,"title":588,"titles":589,"content":590,"level":376},"\u002Fen\u002Fdocs\u002Fdesign\u002Fcolor#board-and-ruler","Board and ruler",[34],"The board surface canvas-surface sits just one step below the panels' white. The board must not be darker than the panels — a dark board pulls attention away from the artifact. 020406080100120140160180200220020\n    mm\n    \n    \n      \n    \n    Artifact 19.2:1 · ruler numbers 2.47:1 · major ticks 1.64:1 · grid 1.22:1\n  \n  The ruler is instrument chrome in three descending layers: numbers 2.47:1 · major ticks 1.64:1 · minor ticks and grid 1.22:1. Against the artifact's own 19.2:1, that ladder says exactly one thing — legible when you look for it, invisible when you don't.",{"id":592,"title":593,"titles":594,"content":595,"level":376},"\u002Fen\u002Fdocs\u002Fdesign\u002Fcolor#brand-and-emphasis","Brand and emphasis",[34],"Regenerate\n    Compare with previous\n    Save parameters\n    Export cut file\n  Brand sits one step darker than body text — the primary button, the filled part of a slider and the on state of a switch should outweigh the text, or the hierarchy inverts. The emphasis red appears at most once per screen, on whatever costs money, spends credits, or cannot be undone. Its force comes entirely from scarcity: two red buttons on one screen is the same as none.",{"id":597,"title":598,"titles":599,"content":600,"level":379},"\u002Fen\u002Fdocs\u002Fdesign\u002Fcolor#semantic-colours-come-in-pairs","Semantic colours come in pairs",[34,593],"Lots of pieces, so the preview is computed in stages. The exported cut file is unaffected.Parameters saved.The shortest edge of a piece is 9.52 mm. Below 12 mm the pieces get hard to handle and snap easily. Use fewer rows and columns, or enlarge the board.Width must be between 40 and 900 mm.Subtle tint for the fill, the solid colour for the icon, and the message itself stays text-primary. Coloured text on a tinted background is the most common accessibility mistake in status messaging — warning on warning-subtle looks tidy and lands well under 4.5:1.",{"id":37,"title":38,"titles":602,"content":603,"level":394},[],"Inter throughout, three weights, and line heights in even pixels rather than unitless multipliers — the vertical rhythm has to survive nesting inside dense control rows, and multipliers drift with font size.",{"id":605,"title":38,"titles":606,"content":603,"level":394},"\u002Fen\u002Fdocs\u002Fdesign\u002Ftypography#typography",[],{"id":608,"title":609,"titles":610,"content":611,"level":376},"\u002Fen\u002Fdocs\u002Fdesign\u002Ftypography#three-registers-not-one-size-ramp","Three registers, not one size ramp",[38],"title · 16\u002F600\u002F22\n      Puzzle Cutline\n      App bar only\n    field-strong · 14\u002F500\u002F20\n      Section header · value 200 mm\n      Structure and values\n    field · 14\u002F400\u002F20\n      Field label: tab size\n      Labels\n    label · 14\u002F600\u002F20\n      Button label\n      Buttons only\n    note · 12\u002F400\u002F18\n      \n        Affects line width in the cut file only; the actual kerf is set by power and focus. The 18px line height is what keeps four lines from suffocating.\n      Paragraph helper text\n    micro · 12\u002F400\u002F16\n      mm · 14:32 · 24px dense controls\n      Units and time\n    ruler · 9\u002F400\n      0 20 40 60 80 100\n      Ruler only\n  Structure and explanation are two registers and should not be scaled together. 12px is not \"14 one size smaller\"; it is the step for paragraph helper text — line height 18 rather than 16, precisely because four lines of 12px on a 16px rhythm suffocate. Rescaling the whole system takes two groups of variables.",{"id":613,"title":614,"titles":615,"content":616,"level":376},"\u002Fen\u002Fdocs\u002Fdesign\u002Ftypography#build-hierarchy-with-weight-and-colour-not-size","Build hierarchy with weight and colour, not size",[38],"Three layers inside a single 14px. What separates the field label from the value is one step of weight plus one of colour — clearer than adding another size, and it doesn't make the panel taller. Pieces14\u002F500 primary\n      Columns\n      30 pieces, 33.33 × 30.00 mm each.\n    Section header 14\u002F500 primary → field label 14\u002F400 secondary → helper text 12\u002F400 tertiary. Three layers, one size.",{"id":41,"title":42,"titles":618,"content":619,"level":394},[],"Board first. The board is not a card; it is the full-bleed layer underneath. The rails sit against it, each responsible for one job.",{"id":621,"title":42,"titles":622,"content":623,"level":394},"\u002Fen\u002Fdocs\u002Fdesign\u002Flayout#layout",[],"Board first. The board is not a card; it is the full-bleed layer underneath. The rails sit against it, each responsible for one job. The leading rail is about what to add — templates, presets, assets, layers. It answers \"what goes on this board\", so its content is a set of interchangeable items shown as thumbnails or a list, with an unmistakable selected state. The trailing rail is about what to adjust — the property stack of the current object. It answers \"what does the thing on the board look like\", so its content is sections and field rows, with export pinned to its bottom rather than hidden at the end of a scroll. That division is why the two rails are not interchangeable: adding is discrete choice, adjusting is continuous tuning. Merge them into one column and every value change makes the user re-find their place between thumbnails and sliders. Derived results (material usage, run time, piece count) don't get a column of their own — they sit with export at the bottom of the trailing rail, or are drawn straight onto the board as dimension labels. Templates \n        \n          Classic puzzle\n          Hexagon\n          Wave cut\n        \n      \n      \n        Regenerate\n        Preview \u002F Wireframe\n        100%\n        \n          \n            \n          \n        \n        200 mm\n        Board · takes everything that's left\n      \n      \n        Parameters \n        \n          Board \n          Width200\n          Height150\n          Pieces \n          Columns6\n          Rows5\n        \n        \n          Total cut length3.75 m\n          ExportFree\n        \n      \n    Three columns, zero gap, zero radius. Rail widths are fixed (leading 320 · trailing 272) and the board absorbs the window — a parameter panel should not get wider just because the window did. Either rail collapses to a 40px rail; collapsing the trailing rail grows the board, and collapsing the leading rail suits the stage where the template is already chosen and it is pure parameter work. A generator with no content library (parameters only, nothing to pick from) should drop the leading rail entirely — two columns is enough, and an extra empty panel is worse than one column fewer.",{"id":625,"title":626,"titles":627,"content":628,"level":376},"\u002Fen\u002Fdocs\u002Fdesign\u002Flayout#the-main-flow-has-to-be-obvious-at-a-glance","The main flow has to be obvious at a glance",[42],"Input → configure → generate → review → adjust → export. Not every generator has every step: a live-preview generator has no \"generate\" step at all, and giving it a generate button is a defect, not a courtesy.",{"id":630,"title":631,"titles":632,"content":633,"level":376},"\u002Fen\u002Fdocs\u002Fdesign\u002Flayout#one-vertical-rhythm-one-ladder","One vertical rhythm, one ladder",[42],"Divider\n    Pieces↑ 24\n    ↑ 12\n    Columns\n      \n    \n    \n    ↑ 16\n    Rows\n      \n    ↑ 4\n    \n    \n    \n  The contrast between 4 and 16 makes helper text unambiguously belong to the control above it; 12 \u003C 16 makes a header unambiguously belong to the group below it. Panel side gutters are 16px throughout. If a layout only looks right at 14px, the problem is alignment, not the scale.",{"id":635,"title":636,"titles":637,"content":638,"level":376},"\u002Fen\u002Fdocs\u002Fdesign\u002Flayout#group-with-lines-not-boxes","Group with lines, not boxes",[42],"Board\n        \n      \n        Pieces\n        \n      \n        Joints\n        \n    \n  Group with dividers inside a panel. The panel is already an enclosed region — drawing the boundary once is enough — and a card's radius competes with the input's radius for attention. Save cards for things that genuinely leave the plane: overlays, select popovers, dialogs. Those need a stroke-default plus a contact shadow to explain where they are floating.",{"id":640,"title":641,"titles":642,"content":643,"level":376},"\u002Fen\u002Fdocs\u002Fdesign\u002Flayout#the-preview-has-three-states-and-all-three-need-designing","The preview has three states, and all three need designing",[42],"StateWhat it must showWhy\n    \n      InitialA sample result or the most recent oneAn empty rectangle reads as \"broken\"\n      ComputingStage name + a number + a way to cancelNever an indefinite spinner. A spinner with personality is what you build when you don't know the progress — here you do\n      DoneZoom, pan, compare, regenerateTuning parameters is a loop, not a one-way trip An error belongs next to the control that produced it, stating the cause and the next step. A toast at the top of the screen is where error messages go to be ignored.",{"id":645,"title":646,"titles":647,"content":648,"level":376},"\u002Fen\u002Fdocs\u002Fdesign\u002Flayout#sizing-and-scrolling","Sizing and scrolling",[42],"Your size is set by the area the platform gives you, not by the viewport. Adapt panels with container queries, not media queries. Below 960px, switch to a stepped flow with a bottom toggle and pin the generate action to the bottom — and clear the vertical dividers when you do. Three engineering details. ①  A panel must be  flex:1; writing only  min-height:0  leaves the container at content height, so the preview column doesn't fill and the parameter column overflows instead of scrolling inside. ② Use a thin scrollbar (8px, drawn yourself) — a classic scrollbar eats 15px out of the padding and makes the right side of the panel look twice as wide as the left. ③ Never scroll horizontally, and never nest a scroll container inside a scroll container.",{"id":45,"title":46,"titles":650,"content":651,"level":394},[],"Depth comes from surface steps and strokes first, and from shadows second. There are three shadows; adding a fourth is the moment a design system starts to leak.",{"id":653,"title":46,"titles":654,"content":655,"level":394},"\u002Fen\u002Fdocs\u002Fdesign\u002Felevation#elevation-and-depth",[],"Depth comes from surface steps and strokes first, and from shadows second. There are three shadows; adding a fourth is the moment a design system starts to leak. subtle\n      0 8px 28px \u002F 10%Dialogs, select popovers\n    micro\n      0 1px 3px \u002F 8%Canvas overlays, the sheet, the active segment\n    \n        \n      \n      0 1px 2px \u002F 20%Slider knob, switch knob\n  knob exists for one specific reason: a white circle on a light track dissolves under micro alone. The knob is the thing that carries the value's position through its shape, so dissolving means the value becomes unreadable. Don't substitute micro, and don't heavy micro globally to rescue the knob — that dirties every overlay. Shadows never animate on hover.",{"id":657,"title":658,"titles":659,"content":660,"level":376},"\u002Fen\u002Fdocs\u002Fdesign\u002Felevation#focus-is-its-own-layer-and-stays-out-of-the-elevation-system","Focus is its own layer and stays out of the elevation system",[46],"Primary\n    Secondary\n    \n  The focus ring is 2px wide with a 2px offset. The offset is load-bearing, not decorative: it separates the ring from the control's own fill. Run the numbers — with the offset, the ring's adjacent colour is the panel's white, 6.08:1; sitting directly against a near-black fill it is 2.92:1, short of 3:1. The input family is the one exception to the ring: inputs, value pills and select triggers show focus by switching their stroke from stroke-input to brand-default. They already have a stroke, so a ring would be a second boundary. The input family is the exception to the focus ring — inputs, value pills and select triggers show focus by switching their stroke from stroke-input to brand-default. They already have a stroke, so a ring would be a second boundary.",{"id":49,"title":50,"titles":662,"content":663,"level":394},[],"Four rounded-rectangle steps, with radius tracking height. That correspondence matters more than any individual number.",{"id":665,"title":50,"titles":666,"content":667,"level":394},"\u002Fen\u002Fdocs\u002Fdesign\u002Fshapes#shape",[],"Four rounded-rectangle steps, with radius tracking height. That correspondence matters more than any individual number. 6\n      24px controls\n    8\n      28 \u002F 32px\n    10\n      40px\n    12\n      Cards \u002F dialogs \u002F overlays\n    \n      \n      The one pill exception\n  A 40px control with a 6px radius looks unfinished; the same control at 20px looks like a different product. Primary controls are never pills — even the largest button caps at 10px. full is reserved for things that are genuinely circular: status dots, the slider knob, the switch knob. The switch track is the one pill exception, and it earns it: a rounded-rectangle switch reads as a checkbox. Radius is the only shape device. No bevels, no notches, no decorative borders, no gradient strokes.",{"id":53,"title":54,"titles":669,"content":670,"level":394},[],"Controls are built on four heights — 24, 28, 32, 40. 32 is the default for buttons; 40 is the default for the input family.",{"id":672,"title":54,"titles":673,"content":674,"level":394},"\u002Fen\u002Fdocs\u002Fdesign\u002Fcomponents#components",[],"Controls are built on four heights — 24, 28, 32, 40. 32 is the default for buttons; 40 is the default for the input family. Every interactive element supports the same state set, and that set is closed: default, hover, active, focus-visible, disabled, loading, selected, error. There are no others. A control that needs a new state is a control that needs rethinking.",{"id":676,"title":677,"titles":678,"content":679,"level":376},"\u002Fen\u002Fdocs\u002Fdesign\u002Fcomponents#buttons","Buttons",[54],"Primary\n    Secondary\n    Ghost\n    Disabled\n    Clear history\n    Export cut file\n  Destructive actions use the secondary shell with error text plus a confirmation step — never a solid red fill. A big red \"Delete\" invites exactly the misclick it was meant to prevent. Button labels are verbs: Generate, Export, Regenerate — not \"OK\", not \"Submit\". Loading keeps the width and the label, swapping only the leading icon for a spinner and setting aria-busy.",{"id":681,"title":682,"titles":683,"content":684,"level":376},"\u002Fen\u002Fdocs\u002Fdesign\u002Fcomponents#the-input-family-white-fill-stroke","The input family: white fill + stroke",[54],"Width must be between 40 and 900 mm.\n    \n  The whole input family is white fill plus a 1px stroke-input stroke. White with a stroke says \"you can type in here\" most clearly, which frees the grey background-input for value pills and segmented tracks. State lives entirely in the stroke: hover lifts to stroke-strong, focus to brand-default, error to error. The error message goes next to the field.",{"id":686,"title":687,"titles":688,"content":689,"level":376},"\u002Fen\u002Fdocs\u002Fdesign\u002Fcomponents#value-pills-one-shape-four-widths","Value pills: one shape, four widths",[54],"Columns\n      Randomness\n        %\n      Corner radius\n        mm\n      Seed\n    Uniformity belongs to the shape; width follows the content. All four pills share the same fill, stroke, radius, height, alignment and weight; only the width steps with the content. The visual anchor is the right edge, and the layout keeps that aligned — so a ragged left edge doesn't hurt reading, while a single digit stranded in a three-digit box is glaring. Keep the slack in each step under 13px. StepValueMade ofUsed for\n    \n      chip-w-sm48px10 + two digits 17 + 10Plain integers, no unit\n      chip-w64px10 + 17 + unit 26–34Two digits + % or mm\n      chip-w-lg80px10 + three digits 25 + shared slot 32With a drag handle\n      Custom88px+By digit countSix digits or more (a seed, say)\n    \n  What is uniform is the shape, not the width. The shape — white fill, stroke-input stroke, 8px radius, 32 tall, right-aligned, weight 500, tabular-nums — must match exactly; width steps with the content, with under 13px of slack in each step.",{"id":691,"title":692,"titles":693,"content":694,"level":376},"\u002Fen\u002Fdocs\u002Fdesign\u002Fcomponents#horizontal-scrubbing-on-numeric-fields","Horizontal scrubbing on numeric fields",[54],"Never use the native up\u002Fdown spinners: the targets are small and the register is wrong for a tool panel. Use a 24×24 horizontal drag handle at the end instead. Hover the pills below — the unit fades out and the handle fades in. Width\n      mm\n      \n        \n    Seed\n      \n      \n        \n  The unit suffix and the drag handle share one slot. Giving each its own leaves a permanent 24px of white space at the right of every pill (the handle is invisible until hover), and you don't need to read the unit at the moment you're about to drag. The rest of the rules: movement under 3px counts as a click → focus and select all · Shift gives a 10× step · setPointerCapture so dragging outside the panel doesn't drop the gesture · in RTL, dragging left increases · removing the native spinner removes coarse keyboard adjustment, so Shift + ↑\u002F↓ = ten steps must be restored · the handle is aria-hidden and out of the tab order · on touch devices the handle is always visible · no handle when the row already has a slider.",{"id":696,"title":697,"titles":698,"content":699,"level":376},"\u002Fen\u002Fdocs\u002Fdesign\u002Fcomponents#select-a-custom-listbox","Select: a custom listbox",[54],"Classic joints\n        \n      \n      \n        Classic joints\n          \n        Wide tabs\n          \n        Wave\n          \n      \n    \n  A native \u003Cselect> popup cannot be skinned, so a slab of system UI drops out of your tool panel. Hence the custom build — and the price is repaying the entire keyboard and screen-reader contract, which is not optional. The selected state is background-active plus a checkmark: a background tint alone is too weak in a four-item list, and colour must never carry meaning by itself. Contract itemRequirement\n    \n      Rolesrole=\"combobox\" \u002F listbox \u002F option, with  aria-expanded、aria-selected、aria-activedescendant\n      Focus modelARIA 1.2 select-only combobox: focus stays on the trigger throughout — no focus transfer (moving focus makes it far easier to strand focus on `body` when the popover closes)\n      Keys when closed↑↓ \u002F Enter \u002F Space \u002F Home \u002F End \u002F any character → open\n      Keys when open↑↓ to move · Home\u002FEnd to the ends · Enter\u002FSpace to select and close · Escape to close without changing the value · Tab closes · click outside closes · type-ahead by first letter\n      Returning focusGive focus back to the trigger on close\n      Where the popover mountsMount to  body using  position:fixed — left in place it gets clipped by scroll containers and  overflow:hidden . Flip upward when there isn't room, and clamp inside the viewport. In RTL, align to the trigger's right edge\n      No-script fallbackKeep a native  \u003Cselect> in the HTML, styled to match the trigger\n      The mobile costYou lose the system picker, and 32px options are small for touch. If phones are a big part of your usage, fall back to the native",{"id":701,"title":702,"titles":703,"content":704,"level":376},"\u002Fen\u002Fdocs\u002Fdesign\u002Fcomponents#slider-three-load-bearing-numbers","Slider: three load-bearing numbers",[54],"Three numbers carry the load: a 6px track, track-off (neutral-300) for the unfilled part, and an 18px knob with a 1px stroke-strong stroke and the knob shadow. The unfilled part's 1.47:1 works because of area — the same ratio drawn as a hairline would vanish, drawn as a 6px band it reads. Filled against unfilled is 12:1, so the position is legible at a glance. The knob's stroke follows the same logic: a white circle on a light track dissolves under a shadow alone, and the knob is what carries the value's position through its shape. The slider is the workhorse of a parameter-driven generator, and it is never used alone. The current value is always shown as a number at the end of the row — a knob position is not a value. When precision matters (step counts, seeds, dimensions) pair it with a numeric field, because you cannot type into a slider. Step and range come from the model's real constraints, not from what \"feels smooth\"; a value that gets silently clamped is a bug the user cannot see. The disabled state should read as \"present but not adjustable\", not as a control that vanished.",{"id":706,"title":707,"titles":708,"content":709,"level":376},"\u002Fen\u002Fdocs\u002Fdesign\u002Fcomponents#switch","Switch",[54],"Tray\n    On\n  Structurally this is a real \u003Cinput type=\"checkbox\">, and the track and knob are only decoration painted on top of it. Every decorative layer needs pointer-events: none so clicks land on the real input underneath, and the whole thing goes inside its \u003Clabel> so the hit area covers the text. The 24px track height is exactly the minimum target size.",{"id":711,"title":712,"titles":713,"content":714,"level":376},"\u002Fen\u002Fdocs\u002Fdesign\u002Fcomponents#segmented-control-and-section-header","Segmented control and section header",[54],"Preview\n      Wireframe\n    \n    \n      \n        Joints\n        \n          \n      \n      \n    \n  Segmented control: grey track, 3px padding, 10px radius, with the active segment white plus micro. It is not a tab set and does not switch pages. Section header: the whole row is one \u003Cbutton aria-expanded>, with the title at the start and the chevron at the end of the row — that way titles line up with field labels on one line and chevrons line up with value pills on another. The chevron points up\u002Fdown rather than left\u002Fright, because vertical directions don't mirror in RTL and that saves a special case. Only hidden and one transform change; there is no height animation.",{"id":716,"title":717,"titles":718,"content":719,"level":376},"\u002Fen\u002Fdocs\u002Fdesign\u002Fcomponents#be-stingy-with-helper-text","Be stingy with helper text",[54],"Tab size\n        %\n      Randomness\n        %\n      Line width\n      The actual kerf is set by power and focus.\n    Be stingy with helper text. Anything that fits in a unit suffix should not become a sentence — 20 % beats \"percentage of the piece's edge length\"; anything readable in the results column should not be repeated in the parameters column. Tertiary text suits a one-line hint, not a paragraph. The ideal end state is two or three genuinely constraint-bearing notes in the entire panel, like the one above — it states a production fact rather than restating the label.",{"id":57,"title":58,"titles":721,"content":722,"level":394},[],"Motion confirms that something changed. It never carries meaning by itself, and it never makes anyone wait.",{"id":724,"title":58,"titles":725,"content":726,"level":394},"\u002Fen\u002Fdocs\u002Fdesign\u002Fmotion#motion",[],"Motion confirms that something changed. It never carries meaning by itself, and it never makes anyone wait. fast · 150ms\n        hover, press, toggle, focus, stroke colour change\n      base · 200ms\n        panel expand, dialog enter\u002Fexit, drawer\n      cubic-bezier(.2,0,.38,1)\n        the one curve used everywhere\n    Animate opacity and transform only — in a preview-heavy interface where the GPU is already busy, animating height or width will drop frames. Nothing over 200ms, no bounce, no overshoot, no lingering. Generation progress is not an animation; it is a number and a stage name. No entrance animation on load: your app opens inside a container that is already visible, so a fade-in only reads as slow. prefers-reduced-motion collapses every duration to zero and nothing is lost, because no state was ever expressed by motion alone.",{"id":61,"title":62,"titles":728,"content":729,"level":394},[],"Icons are inline stroke SVGs, not an icon font.",{"id":731,"title":62,"titles":732,"content":733,"level":394},"\u002Fen\u002Fdocs\u002Fdesign\u002Ficon#icons",[],"Icons are inline stroke SVGs, not an icon font. Icons are inline stroke SVGs. The spec: fill:none · stroke:currentColor · stroke-width:1.5 · linecap\u002Flinejoin:round, with colour always inherited from the parent's semantic text token — an icon that ships its own hard-coded colour is an icon that will be wrong in the next state. No icon font: it is an external dependency, and when it fails to load every icon-only button becomes an empty square. Icons appear too often to gamble on an external dependency.",{"id":735,"title":736,"titles":737,"content":738,"level":379},"\u002Fen\u002Fdocs\u002Fdesign\u002Ficon#four-hit-area-steps-with-the-glyph-following-along","Four hit-area steps, with the glyph following along",[62],"20 \u002F 16\n    \n      24 \u002F 18\n    \n      32 \u002F 20 · default\n    \n      40 \u002F 24\n  The 20px step is below the accessibility minimum and may only be used when the same action is also reachable through a target that does meet 24px. Note that the step names collide with control heights but not with their values: a mini control is 24px tall, while a mini icon button is 20px square.",{"id":740,"title":741,"titles":742,"content":743,"level":379},"\u002Fen\u002Fdocs\u002Fdesign\u002Ficon#three-kinds-of-icon-three-obligations","Three kinds of icon, three obligations",[62],"TypeObligationWhy\n    \n      Decorativearia-hidden=\"true\"It sits next to its own text label, and would otherwise be announced twice\n      Interactivearia-labelDescribe the action rather than the picture: 「New seed」, not 「Dice」\n      IndicativeScreen-reader textIcons like status dots, where neither the colour nor the shape carries meaning on its own Directional icons — arrows, chevrons, back and forward, indent, undo — need a class that lets RTL flip them. Clocks, checkmarks, play buttons, logos and charts with a time axis must never flip. Up\u002Fdown arrows never need flipping, so prefer them whenever you have the choice.",{"id":65,"title":66,"titles":745,"content":746,"level":394},[],"The platform renders your generator inside a document whose dir follows the user's language. Mirroring is not optional, and there is no second stylesheet — the layout has to flip on its own.",{"id":748,"title":66,"titles":749,"content":750,"level":394},"\u002Fen\u002Fdocs\u002Fdesign\u002Fdirection#direction-rtl",[],"The platform renders your generator inside a document whose dir follows the user's language. Mirroring is not optional, and there is no second stylesheet — the layout has to flip on its own. Compare side by side with the LTR \u002F RTL toggle above the demo below. Written the modern way this is almost free: use logical properties for every one-sided value — margin-inline-start, padding-inline-end, inset-inline-start, text-align: start. Symmetric shorthands need no attention at all, and flex and grid mirror themselves; adding flex-row-reverse for RTL flips a layout that has already been flipped. Board\n        عرض اللوح\n          mm\n        Board height\n          mm\n          \n            \n        \n        Tray\n      \n      \n        Regenerate\n        \n          050100150200250300350400450500550600650700750\n          \n            \n          200 mm\n        \n      \n    Hit the toggle and watch what flips and what does not. Flips: the rail changes sides, label and value swap, the slider fills from the other end, the switch knob travels the other way — all of it done by logical properties and flex on their own, without a single flex-row-reverse. Does not flip: the ruler's zero point, the tick direction, the board's position. The Arabic label on the first row (عرض اللوح, board width) is the text itself running right to left: in LTR عرض sits on the left and اللوح on the right, and RTL swaps the two words. The Latin row below it only moves as a block — its letters still run left to right, and 200 is never 002. Direction changes the arrangement, not the writing direction inside a word. The board, the ruler and the artifact never mirror — lock that layer to direction: ltr and the logical properties inside it always resolve to the left, so the artifact's orientation is independent of the interface around it. But the overlays on the canvas are interface, not artifact, so they do flip: in RTL, Regenerate lands on the mirrored side.",{"id":752,"title":753,"titles":754,"content":755,"level":379},"\u002Fen\u002Fdocs\u002Fdesign\u002Fdirection#pitfalls-this-system-actually-hit","Pitfalls this system actually hit",[66],"SymptomCause and fix\n    \n      The slider knob moves the wrong wayThe webkit fill uses  linear-gradient(to right …), which has to become  to left\n      Scrubbing changes the value in the wrong directionRead  direction  and flip the sign of the delta\n      The select popover is misalignedWhen positioning in JS, RTL must align with the trigger's right edge, not blindly reuse  left\n      A centred floating chip sits half offDon't use  inset-inline-start:50% + translateX(-50%); use  inset-inline:0; margin-inline:auto\n      The progress bar grows from the wrong endtransform-origin  has to switch sides with the direction\n      The switch knob slides outside its trackThe knob starts from  inset-inline-start  but travels with  translateX(16px)  —  transform  is physical, so RTL needs  -16px. Any transform that means \"move forward\" has to decide its own direction\n      Labels collide with inputsOne-sided padding didn't flip. Use logical properties throughout",{"id":69,"title":70,"titles":757,"content":758,"level":394},[],"This is the floor, not the ceiling.",{"id":760,"title":70,"titles":761,"content":762,"level":394},"\u002Fen\u002Fdocs\u002Fdesign\u002Faccessibility#accessibility",[],"This is the floor, not the ceiling. RequirementDetail\n    \n      Targets ≥ 24×24pxWCAG 2.2 SC 2.5.8。Check the width too, because icon-only buttons are square. Where something is deliberately shorter (a slider rail, a checkbox glyph), grow the hit area rather than shrink the target\n      Verify contrast pair by pairNever by assumption. Recompute every key pairing each time the ramp changes, including the worst case of a small size on a light background\n      Full keyboard coverageEverything the mouse can reach, in a visible order. Trap focus while a dialog is open and return it to the trigger on close\n      [hidden] Must be restoredAny component that sets  display:flex  or  grid  overrides the browser's default  [hidden]{display:none}. Put it back in the base stylesheet [hidden]{display:none !important}——Off-screen panels leaking into view is exactly how this goes wrong\n      Meaning never rests on colour aloneA red stroke needs a message; a spinner needs text; a selected state needs a checkmark",{"id":764,"title":765,"titles":766,"content":767,"level":376},"\u002Fen\u002Fdocs\u002Fdesign\u002Faccessibility#measured-key-pairings","Measured key pairings",[70],"PairingMeasuredThresholdResult\n    Value \u002F body  900  on white\n          17.74:14.5\n          PassField label  600  on white\n          7.56:14.5\n          PassField label  600  on the pill fill\n          6.87:14.5\n          PassHelper text  500  on white\n          4.83:14.5\n          PassHelper text  500  on the board\n          4.71:14.5\n          PassWhite on the primary button\n          17.74:14.5\n          PassWhite on the emphasis red\n          5.27:14.5\n          PassFilled vs unfilled track\n          12.04:13\n          PassFocus ring on white (the adjacent colour after a 2px offset)This is where the offset carries the load: with it, the ring's adjacent colour is the panel's white; without it, the ring sits against a near-black fill and drops to 2.92:1.\n          6.08:13\n          Pass",{"id":769,"title":770,"titles":771,"content":772,"level":376},"\u002Fen\u002Fdocs\u002Fdesign\u002Faccessibility#known-deliberately-accepted-deviations","Known, deliberately accepted deviations",[70],"A spec should record its own debts rather than pretend it has none. ItemMeasuredThresholdWhy\n    \n      stroke-default  as an overlay's only boundary1.24:13:1Overlays also carry a shadow, so the boundary doesn't rest on the stroke alone\n      stroke-input  as an input's only boundary1.47:13:1The main source of the overall feel. Meeting the bar would mean darkening it to around  #949ba5 , which makes edges visibly heavier — ship an override step instead; it is one token\n      Ruler numbers2.47:14.5:1Instrument markings are not content, and the board's dimensions are stated as numbers in the results column",{"id":73,"title":74,"titles":774,"content":775,"level":394},[],"These are not style choices — they are the preconditions for running inside atomm.",{"id":777,"title":74,"titles":778,"content":779,"level":394},"\u002Fen\u002Fdocs\u002Fdesign\u002Fplatform#platform-constraints",[],"These are not style choices — they are the preconditions for running inside atomm. ConstraintRequirement\n    \n      Export goes through the platform buttonPlace  \u003Cdiv data-atomm-export-button>\u003C\u002Fdiv> where you want it and register  atomm.lifecycle.on('export', …). The SDK renders the button, and only its  --atomm-export-*  variables can be themed.A submission with no registered export hook is rejected automatically during review.During development you may render a fallback button in the same slot, but the SDK must always take precedence when present\n      Treat the export as machine-facing1:1 physical size, mm  units, and a viewBox in width=\"200mm\"  coordinates. Cut-line colour may be mapped to a machine operation by the device, so changing it needs its own sign-off and does not ride along with the interface ramp. Reference imagery used for preview never enters the cut file\n      The top bar belongs to the platformBranding, account, credits. Don't rebuild it and don't imitate it. Your generator's own controls (project, save, history) go in a light bar above your own content area\n      You are inside someone else's containerYou cannot cover the platform chrome, you do not know the viewport size, and your stacking context is entirely your own — one overlay layer plus one toast layer is enough, so z-index cap at 3",{"id":77,"title":78,"titles":781,"content":92,"level":394},[],{"id":783,"title":78,"titles":784,"content":92,"level":394},"\u002Fen\u002Fdocs\u002Fdesign\u002Frules#rule-checklist",[],{"id":786,"title":34,"titles":787,"content":788,"level":379},"\u002Fen\u002Fdocs\u002Fdesign\u002Frules#colour",[78],"01Keep the neutral ramp's cool cast consistent and committed. Low-chroma greys land in the ambiguous zone and read as dirty.02Make each of the four text steps about half the one above. Four steps bunched at one end are really two.03Recompute the key pairings every time the ramp changes, including the worst case of a small size on a light background.04Keep the board lighter than the panels. The artifact is the only thing on screen that should be loud.05Judge by measured contrast. At the same ratio, elements that work through area (tracks, bands) and elements that work through line (strokes, ticks) are not equally visible.",{"id":790,"title":147,"titles":791,"content":792,"level":379},"\u002Fen\u002Fdocs\u002Fdesign\u002Frules#hierarchy",[78],"06Build hierarchy by taking weight away: let the label fall back to a lighter secondary, and the value steps forward.07Separate three layers within one size using weight plus colour, rather than adding another size.08Give structural labels and helper text their own registers: one is the skeleton, the other is a footnote.09When the hierarchy is unclear, lighten the label first — the panel gets lighter overall and the hierarchy gets clearer.",{"id":794,"title":189,"titles":795,"content":796,"level":379},"\u002Fen\u002Fdocs\u002Fdesign\u002Frules#shape-and-space",[78],"10Put the uniformity in the shape — identical fill, stroke, radius and height.11Let width follow content, and leave the right edge to the layout's alignment.12Group with dividers inside an already-enclosed panel; save cards for things that genuinely leave the plane.13Let elements that never appear together share one slot (the unit suffix and the drag handle).14Take every value from the 8px scale. A layout that only looks right at 14px has an alignment problem.",{"id":798,"title":241,"titles":799,"content":800,"level":379},"\u002Fen\u002Fdocs\u002Fdesign\u002Frules#controls",[78],"15Spend the red emphasis step on exactly one action per screen, or on none at all.16Treat the state set as closed. A control that seems to need a ninth state actually needs rethinking.17Before rebuilding a native control, write out the keyboard and screen-reader contract you now owe, then repay it item by item.18Put pointer-events: none on decorative layers and leave the clicks to the real control underneath.19Give destructive actions the secondary shell with error text plus a confirmation step.20Put one drag affordance on a row, never two.21Label buttons with verbs: Generate, Export, Regenerate.",{"id":802,"title":313,"titles":803,"content":804,"level":379},"\u002Fen\u002Fdocs\u002Fdesign\u002Frules#engineering",[78],"22Write flex: 1 on panels that must fill, not just min-height: 0.23Ship icons as inline stroke SVGs and depend on no icon font.24Run a dangling-variable check after every token change: deleting a neutral step without updating the semantic token that references it makes some state fail silently.25Use a thin scrollbar so a panel's left and right padding stay symmetrical.26Mount overlays on body with position: fixed so scroll containers cannot clip them.",[806,808],{"title":74,"path":73,"stem":807,"children":-1},"en\u002Fdocs\u002Fdesign\u002F11.platform",null,1785741846690]