diff --git a/.gitattributes b/.gitattributes index b0dda5e..e033cc6 100644 --- a/.gitattributes +++ b/.gitattributes @@ -8,6 +8,7 @@ # Binary assets are never text, whatever the heuristic decides. *.png binary +*.webp binary *.jpg binary *.jpeg binary *.gif binary diff --git a/README.md b/README.md index fe1e610..f2b8a67 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,9 @@ Agent 编程(Claude Code / Codex)的个人基准仓库:统一个人偏好 | explain-interface | [jakubkrehel/skills](https://github.com/jakubkrehel/skills) | 根据网址或截图说明界面、视觉效果或动效是怎么做出来的 | 用户级 | | emil-design-eng | [emilkowalski/skills](https://github.com/emilkowalski/skills) | Emil 的界面打磨、组件取舍和动效判断 | 用户级 | | impeccable | [pbakaus/impeccable](https://github.com/pbakaus/impeccable) | 设计、改版、打磨或评审前端界面,并带上 agents 与 hook | 项目级 | +| unslop | [heydamir/unslop](https://github.com/heydamir/unslop),Damir Sokolovsky([@heydamir](https://x.com/heydamir)) | 把看起来像模型生成的界面改成有产品自己性格的设计 | 用户级 | + +`unslop` 以 CC BY 4.0 发布,使用或改编时保留对 Damir Sokolovsky(@heydamir)的署名,并链接回 [heydamir/unslop](https://github.com/heydamir/unslop)。现场网页快照需要 Playwright 和 Chromium,故事视频还需要 ffmpeg。这两项都是可选的,没装时其余改版步骤不受影响。 ### 动效 @@ -109,6 +112,7 @@ agent-coding-baseline/ ├── vendor/ # 只由 sync-upstream.py 写入:vendor/// │ ├── interface-design/ │ │ ├── emilkowalski-skills/ # emil-design-eng +│ │ ├── heydamir-unslop/ # unslop,以及 LICENSE │ │ ├── impeccable/ # impeccable,以及 agents、hooks、LICENSE、NOTICE │ │ └── jakubkrehel-skills/ # better-colors、better-layout、better-typography、better-ui、better-writing、explain-interface │ ├── motion/ diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index ec34935..957f6e1 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -44,6 +44,15 @@ - 语言与工程:`write-swift` - 未写入 `vendor/` 的根目录文件:`performance-cheatsheet.md`。它不是 skill,其中的动效性能要点已由 `animation-performance` 覆盖。 +## heydamir/unslop → `vendor/interface-design/heydamir-unslop/` + +- 作者:Damir Sokolovsky(@heydamir) +- 来源:https://github.com/heydamir/unslop(https://heydamir.design/unslop) +- 许可证:CC BY 4.0。全文见 `vendor/interface-design/heydamir-unslop/LICENSE`。使用、修改或再分发时需注明 Damir Sokolovsky(@heydamir),并链接回 https://github.com/heydamir/unslop。 +- 用途:把看起来像模型生成的界面改成有产品自己性格的设计,安装为用户级 skill。分类是界面设计。 +- 可选依赖:现场网页快照(`scripts/snapshot.mjs`)需要本机的 Playwright 和 Chromium;故事视频(`scripts/story.mjs`)还需要 ffmpeg。没装时 skill 的其余步骤仍可用。这两项不随仓库安装。 +- 同步范围:`skills/unslop`(含 `references/`,SKILL.md 依赖其中的说明和参考图)和 `LICENSE`。上游 `examples/` 不进入 `vendor/`。 + ## 参考整理(未同步进 vendor/) ### enesozturk/dimensional-drawing diff --git a/docs/UPSTREAMS.md b/docs/UPSTREAMS.md index 496cf45..b124b17 100644 --- a/docs/UPSTREAMS.md +++ b/docs/UPSTREAMS.md @@ -48,9 +48,9 @@ - 作者:Paul Bakaus - 许可证:Apache-2.0(文件 `LICENSE`) - 安装范围:项目级,安装到目标项目的 `.claude/` -- 锁定提交:[`df3a4ab`](https://github.com/pbakaus/impeccable/commit/df3a4abe7d0840d5f60324812c432e00b66a4e75)(`df3a4abe7d0840d5f60324812c432e00b66a4e75`) -- 上游提交时间:2026-10-30T10:16:26+00:00 -- 同步时间:2026-10-31T04:41:04+00:00 +- 锁定提交:[`e89e3ad`](https://github.com/pbakaus/impeccable/commit/e89e3ada3e875a89ac2c5d33d11e65ebef6fe285)(`e89e3ada3e875a89ac2c5d33d11e65ebef6fe285`) +- 上游提交时间:2026-11-01T14:26:36+00:00 +- 同步时间:2026-11-01T15:03:43+00:00 | Skill | 分类 | | --- | --- | @@ -116,6 +116,29 @@ | --- | --- | | `performance-cheatsheet.md` | 不是 skill。其中的动效性能要点已由 animation-performance 覆盖。 | +### heydamir/unslop + +- 清单 id:`heydamir-unslop` +- 仓库:https://github.com/heydamir/unslop +- 主页:https://heydamir.design/unslop +- 作者:Damir Sokolovsky (@heydamir) +- 许可证:CC BY 4.0(文件 `LICENSE`) +- 许可证说明:CC BY 4.0 要求署名 Damir Sokolovsky(@heydamir),并链接回 https://github.com/heydamir/unslop。 +- 安装范围:用户级,安装到 `~/.claude/skills//` +- 锁定提交:[`1d13095`](https://github.com/heydamir/unslop/commit/1d13095e4bef6ad6c74f1dd828d84c4ba309a4dc)(`1d13095e4bef6ad6c74f1dd828d84c4ba309a4dc`) +- 上游提交时间:2026-11-01T03:05:59+03:00 +- 同步时间:2026-11-01T15:03:44+00:00 + +| Skill | 分类 | +| --- | --- | +| `unslop` | 界面设计(`interface-design`) | + +#### 未收录 + +| 路径 | 原因 | +| --- | --- | +| `examples` | 示例页面和配图。skill 的 references/ 已包含 SKILL.md 依赖的说明和参考图,examples/ 不进入 vendor/。 | + ## 参考整理 这些资料不进 `vendor/`,不锁定提交,不由 `scripts/sync-upstream.py` 安装。 diff --git a/skills.json b/skills.json index 5ec07fa..69e4898 100644 --- a/skills.json +++ b/skills.json @@ -179,6 +179,29 @@ "reason": "不是 skill。其中的动效性能要点已由 animation-performance 覆盖。" } ] + }, + { + "id": "heydamir-unslop", + "repository": "https://github.com/heydamir/unslop", + "homepage": "https://heydamir.design/unslop", + "author": "Damir Sokolovsky (@heydamir)", + "license": "CC BY 4.0", + "licenseFile": "LICENSE", + "$licenseNote": "CC BY 4.0 要求署名 Damir Sokolovsky(@heydamir),并链接回 https://github.com/heydamir/unslop。", + "paths": { + "skills/unslop": "skills/unslop", + "LICENSE": "LICENSE" + }, + "skills": [ + {"name": "unslop", "category": "interface-design"} + ], + "scope": "user", + "excluded": [ + { + "path": "examples", + "reason": "示例页面和配图。skill 的 references/ 已包含 SKILL.md 依赖的说明和参考图,examples/ 不进入 vendor/。" + } + ] } ], "references": [ diff --git a/skills.lock.json b/skills.lock.json index b99b871..478c640 100644 --- a/skills.lock.json +++ b/skills.lock.json @@ -8,9 +8,9 @@ }, "impeccable": { "repository": "https://github.com/pbakaus/impeccable", - "commit": "df3a4abe7d0840d5f60324812c432e00b66a4e75", - "committedAt": "2026-10-30T10:16:26+00:00", - "syncedAt": "2026-10-31T04:41:04+00:00" + "commit": "e89e3ada3e875a89ac2c5d33d11e65ebef6fe285", + "committedAt": "2026-11-01T14:26:36+00:00", + "syncedAt": "2026-11-01T15:03:43+00:00" }, "vercel-agent-skills": { "repository": "https://github.com/vercel-labs/agent-skills", @@ -23,6 +23,12 @@ "commit": "a81dbca07f2217b7103e510c0f13f8d5b017afc9", "committedAt": "2026-10-28T19:25:28+01:00", "syncedAt": "2026-10-31T04:41:04+00:00" + }, + "heydamir-unslop": { + "repository": "https://github.com/heydamir/unslop", + "commit": "1d13095e4bef6ad6c74f1dd828d84c4ba309a4dc", + "committedAt": "2026-11-01T03:05:59+03:00", + "syncedAt": "2026-11-01T15:03:44+00:00" } } } diff --git a/vendor/interface-design/heydamir-unslop/LICENSE b/vendor/interface-design/heydamir-unslop/LICENSE new file mode 100644 index 0000000..da6ab6c --- /dev/null +++ b/vendor/interface-design/heydamir-unslop/LICENSE @@ -0,0 +1,396 @@ +Attribution 4.0 International + +======================================================================= + +Creative Commons Corporation ("Creative Commons") is not a law firm and +does not provide legal services or legal advice. Distribution of +Creative Commons public licenses does not create a lawyer-client or +other relationship. Creative Commons makes its licenses and related +information available on an "as-is" basis. Creative Commons gives no +warranties regarding its licenses, any material licensed under their +terms and conditions, or any related information. Creative Commons +disclaims all liability for damages resulting from their use to the +fullest extent possible. + +Using Creative Commons Public Licenses + +Creative Commons public licenses provide a standard set of terms and +conditions that creators and other rights holders may use to share +original works of authorship and other material subject to copyright +and certain other rights specified in the public license below. The +following considerations are for informational purposes only, are not +exhaustive, and do not form part of our licenses. + + Considerations for licensors: Our public licenses are + intended for use by those authorized to give the public + permission to use material in ways otherwise restricted by + copyright and certain other rights. Our licenses are + irrevocable. Licensors should read and understand the terms + and conditions of the license they choose before applying it. + Licensors should also secure all rights necessary before + applying our licenses so that the public can reuse the + material as expected. Licensors should clearly mark any + material not subject to the license. This includes other CC- + licensed material, or material used under an exception or + limitation to copyright. More considerations for licensors: + wiki.creativecommons.org/Considerations_for_licensors + + Considerations for the public: By using one of our public + licenses, a licensor grants the public permission to use the + licensed material under specified terms and conditions. If + the licensor's permission is not necessary for any reason--for + example, because of any applicable exception or limitation to + copyright--then that use is not regulated by the license. Our + licenses grant only permissions under copyright and certain + other rights that a licensor has authority to grant. Use of + the licensed material may still be restricted for other + reasons, including because others have copyright or other + rights in the material. A licensor may make special requests, + such as asking that all changes be marked or described. + Although not required by our licenses, you are encouraged to + respect those requests where reasonable. More considerations + for the public: + wiki.creativecommons.org/Considerations_for_licensees + +======================================================================= + +Creative Commons Attribution 4.0 International Public License + +By exercising the Licensed Rights (defined below), You accept and agree +to be bound by the terms and conditions of this Creative Commons +Attribution 4.0 International Public License ("Public License"). To the +extent this Public License may be interpreted as a contract, You are +granted the Licensed Rights in consideration of Your acceptance of +these terms and conditions, and the Licensor grants You such rights in +consideration of benefits the Licensor receives from making the +Licensed Material available under these terms and conditions. + + +Section 1 -- Definitions. + + a. Adapted Material means material subject to Copyright and Similar + Rights that is derived from or based upon the Licensed Material + and in which the Licensed Material is translated, altered, + arranged, transformed, or otherwise modified in a manner requiring + permission under the Copyright and Similar Rights held by the + Licensor. For purposes of this Public License, where the Licensed + Material is a musical work, performance, or sound recording, + Adapted Material is always produced where the Licensed Material is + synched in timed relation with a moving image. + + b. Adapter's License means the license You apply to Your Copyright + and Similar Rights in Your contributions to Adapted Material in + accordance with the terms and conditions of this Public License. + + c. Copyright and Similar Rights means copyright and/or similar rights + closely related to copyright including, without limitation, + performance, broadcast, sound recording, and Sui Generis Database + Rights, without regard to how the rights are labeled or + categorized. For purposes of this Public License, the rights + specified in Section 2(b)(1)-(2) are not Copyright and Similar + Rights. + + d. Effective Technological Measures means those measures that, in the + absence of proper authority, may not be circumvented under laws + fulfilling obligations under Article 11 of the WIPO Copyright + Treaty adopted on December 20, 1996, and/or similar international + agreements. + + e. Exceptions and Limitations means fair use, fair dealing, and/or + any other exception or limitation to Copyright and Similar Rights + that applies to Your use of the Licensed Material. + + f. Licensed Material means the artistic or literary work, database, + or other material to which the Licensor applied this Public + License. + + g. Licensed Rights means the rights granted to You subject to the + terms and conditions of this Public License, which are limited to + all Copyright and Similar Rights that apply to Your use of the + Licensed Material and that the Licensor has authority to license. + + h. Licensor means the individual(s) or entity(ies) granting rights + under this Public License. + + i. Share means to provide material to the public by any means or + process that requires permission under the Licensed Rights, such + as reproduction, public display, public performance, distribution, + dissemination, communication, or importation, and to make material + available to the public including in ways that members of the + public may access the material from a place and at a time + individually chosen by them. + + j. Sui Generis Database Rights means rights other than copyright + resulting from Directive 96/9/EC of the European Parliament and of + the Council of 11 March 1996 on the legal protection of databases, + as amended and/or succeeded, as well as other essentially + equivalent rights anywhere in the world. + + k. You means the individual or entity exercising the Licensed Rights + under this Public License. Your has a corresponding meaning. + + +Section 2 -- Scope. + + a. License grant. + + 1. Subject to the terms and conditions of this Public License, + the Licensor hereby grants You a worldwide, royalty-free, + non-sublicensable, non-exclusive, irrevocable license to + exercise the Licensed Rights in the Licensed Material to: + + a. reproduce and Share the Licensed Material, in whole or + in part; and + + b. produce, reproduce, and Share Adapted Material. + + 2. Exceptions and Limitations. For the avoidance of doubt, where + Exceptions and Limitations apply to Your use, this Public + License does not apply, and You do not need to comply with + its terms and conditions. + + 3. Term. The term of this Public License is specified in Section + 6(a). + + 4. Media and formats; technical modifications allowed. The + Licensor authorizes You to exercise the Licensed Rights in + all media and formats whether now known or hereafter created, + and to make technical modifications necessary to do so. The + Licensor waives and/or agrees not to assert any right or + authority to forbid You from making technical modifications + necessary to exercise the Licensed Rights, including + technical modifications necessary to circumvent Effective + Technological Measures. For purposes of this Public License, + simply making modifications authorized by this Section 2(a) + (4) never produces Adapted Material. + + 5. Downstream recipients. + + a. Offer from the Licensor -- Licensed Material. Every + recipient of the Licensed Material automatically + receives an offer from the Licensor to exercise the + Licensed Rights under the terms and conditions of this + Public License. + + b. No downstream restrictions. You may not offer or impose + any additional or different terms or conditions on, or + apply any Effective Technological Measures to, the + Licensed Material if doing so restricts exercise of the + Licensed Rights by any recipient of the Licensed + Material. + + 6. No endorsement. Nothing in this Public License constitutes or + may be construed as permission to assert or imply that You + are, or that Your use of the Licensed Material is, connected + with, or sponsored, endorsed, or granted official status by, + the Licensor or others designated to receive attribution as + provided in Section 3(a)(1)(A)(i). + + b. Other rights. + + 1. Moral rights, such as the right of integrity, are not + licensed under this Public License, nor are publicity, + privacy, and/or other similar personality rights; however, to + the extent possible, the Licensor waives and/or agrees not to + assert any such rights held by the Licensor to the limited + extent necessary to allow You to exercise the Licensed + Rights, but not otherwise. + + 2. Patent and trademark rights are not licensed under this + Public License. + + 3. To the extent possible, the Licensor waives any right to + collect royalties from You for the exercise of the Licensed + Rights, whether directly or through a collecting society + under any voluntary or waivable statutory or compulsory + licensing scheme. In all other cases the Licensor expressly + reserves any right to collect such royalties. + + +Section 3 -- License Conditions. + +Your exercise of the Licensed Rights is expressly made subject to the +following conditions. + + a. Attribution. + + 1. If You Share the Licensed Material (including in modified + form), You must: + + a. retain the following if it is supplied by the Licensor + with the Licensed Material: + + i. identification of the creator(s) of the Licensed + Material and any others designated to receive + attribution, in any reasonable manner requested by + the Licensor (including by pseudonym if + designated); + + ii. a copyright notice; + + iii. a notice that refers to this Public License; + + iv. a notice that refers to the disclaimer of + warranties; + + v. a URI or hyperlink to the Licensed Material to the + extent reasonably practicable; + + b. indicate if You modified the Licensed Material and + retain an indication of any previous modifications; and + + c. indicate the Licensed Material is licensed under this + Public License, and include the text of, or the URI or + hyperlink to, this Public License. + + 2. You may satisfy the conditions in Section 3(a)(1) in any + reasonable manner based on the medium, means, and context in + which You Share the Licensed Material. For example, it may be + reasonable to satisfy the conditions by providing a URI or + hyperlink to a resource that includes the required + information. + + 3. If requested by the Licensor, You must remove any of the + information required by Section 3(a)(1)(A) to the extent + reasonably practicable. + + 4. If You Share Adapted Material You produce, the Adapter's + License You apply must not prevent recipients of the Adapted + Material from complying with this Public License. + + +Section 4 -- Sui Generis Database Rights. + +Where the Licensed Rights include Sui Generis Database Rights that +apply to Your use of the Licensed Material: + + a. for the avoidance of doubt, Section 2(a)(1) grants You the right + to extract, reuse, reproduce, and Share all or a substantial + portion of the contents of the database; + + b. if You include all or a substantial portion of the database + contents in a database in which You have Sui Generis Database + Rights, then the database in which You have Sui Generis Database + Rights (but not its individual contents) is Adapted Material; and + + c. You must comply with the conditions in Section 3(a) if You Share + all or a substantial portion of the contents of the database. + +For the avoidance of doubt, this Section 4 supplements and does not +replace Your obligations under this Public License where the Licensed +Rights include other Copyright and Similar Rights. + + +Section 5 -- Disclaimer of Warranties and Limitation of Liability. + + a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE + EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS + AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF + ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, + IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, + WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR + PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, + ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT + KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT + ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. + + b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE + TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, + NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, + INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, + COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR + USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN + ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR + DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR + IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. + + c. The disclaimer of warranties and limitation of liability provided + above shall be interpreted in a manner that, to the extent + possible, most closely approximates an absolute disclaimer and + waiver of all liability. + + +Section 6 -- Term and Termination. + + a. This Public License applies for the term of the Copyright and + Similar Rights licensed here. However, if You fail to comply with + this Public License, then Your rights under this Public License + terminate automatically. + + b. Where Your right to use the Licensed Material has terminated under + Section 6(a), it reinstates: + + 1. automatically as of the date the violation is cured, provided + it is cured within 30 days of Your discovery of the + violation; or + + 2. upon express reinstatement by the Licensor. + + For the avoidance of doubt, this Section 6(b) does not affect any + right the Licensor may have to seek remedies for Your violations + of this Public License. + + c. For the avoidance of doubt, the Licensor may also offer the + Licensed Material under separate terms or conditions or stop + distributing the Licensed Material at any time; however, doing so + will not terminate this Public License. + + d. Sections 1, 5, 6, 7, and 8 survive termination of this Public + License. + + +Section 7 -- Other Terms and Conditions. + + a. The Licensor shall not be bound by any additional or different + terms or conditions communicated by You unless expressly agreed. + + b. Any arrangements, understandings, or agreements regarding the + Licensed Material not stated herein are separate from and + independent of the terms and conditions of this Public License. + + +Section 8 -- Interpretation. + + a. For the avoidance of doubt, this Public License does not, and + shall not be interpreted to, reduce, limit, restrict, or impose + conditions on any use of the Licensed Material that could lawfully + be made without permission under this Public License. + + b. To the extent possible, if any provision of this Public License is + deemed unenforceable, it shall be automatically reformed to the + minimum extent necessary to make it enforceable. If the provision + cannot be reformed, it shall be severed from this Public License + without affecting the enforceability of the remaining terms and + conditions. + + c. No term or condition of this Public License will be waived and no + failure to comply consented to unless expressly agreed to by the + Licensor. + + d. Nothing in this Public License constitutes or may be interpreted + as a limitation upon, or waiver of, any privileges and immunities + that apply to the Licensor or You, including from the legal + processes of any jurisdiction or authority. + + +======================================================================= + +Creative Commons is not a party to its public +licenses. Notwithstanding, Creative Commons may elect to apply one of +its public licenses to material it publishes and in those instances +will be considered the “Licensor.” The text of the Creative Commons +public licenses is dedicated to the public domain under the CC0 Public +Domain Dedication. Except for the limited purpose of indicating that +material is shared under a Creative Commons public license or as +otherwise permitted by the Creative Commons policies published at +creativecommons.org/policies, Creative Commons does not authorize the +use of the trademark "Creative Commons" or any other trademark or logo +of Creative Commons without its prior written consent including, +without limitation, in connection with any unauthorized modifications +to any of its public licenses or any other arrangements, +understandings, or agreements concerning use of licensed material. For +the avoidance of doubt, this paragraph does not form part of the +public licenses. + +Creative Commons may be contacted at creativecommons.org. + diff --git a/vendor/interface-design/heydamir-unslop/skills/unslop/SKILL.md b/vendor/interface-design/heydamir-unslop/skills/unslop/SKILL.md new file mode 100644 index 0000000..9a1c49e --- /dev/null +++ b/vendor/interface-design/heydamir-unslop/skills/unslop/SKILL.md @@ -0,0 +1,233 @@ +--- +name: unslop +description: Turns vibecoded, AI-looking UI into a designed interface with its own character. Finds a direction from the product itself, then fixes spacing, color, icons, copy, type, layout, details, visuals and motion in that order, and can generate a real hero visual with an image tool when one is available. Use when the user asks to unslop, polish, clean up or redesign a screen so it stops looking AI-generated, or when a UI shows the usual tells like purple gradients, glows, gradient text, emoji labels, "New ✨" badges, glossy blobs, uppercase tracked labels, pulsing elements or uneven spacing. Works on React, Tailwind, HTML and CSS, for dashboards, apps and landing pages. +license: CC-BY-4.0 +metadata: + author: Damir Sokolovsky + x: https://x.com/heydamir + site: https://heydamir.design/unslop +--- + +# Unslop + +unslop was created by Damir Sokolovsky ([@heydamir](https://x.com/heydamir)), who started the Unslopping series on X. It is licensed under CC BY 4.0, so keep this credit in any copy or adaptation. + +Vibecoded UI fails in the same predictable ways. Every surface glows, every label has an emoji, spacing is whatever the model typed that day, and the copy shouts. Unslopping fixes the craft and keeps the product. The features, content and order of sections stay. What changes is that every size, color, word and image becomes deliberate. + +Removing slop is only half the job. A screen that is clean but could belong to any product is the next template. Every run ends with a screen that only this product could have. + +The difference between slop and craft is where the richness goes. Slop spreads color and effects over the chrome, the backgrounds, buttons, borders and labels. Craft keeps the chrome quiet and puts the richness into one or two moments of content that people remember, like a rendered object, a beautiful chart or an image from the product's world. + +Work like a designer doing a redline pass, one layer at a time, across the whole screen. + +## What good looks like + +- **One bright element.** Each screen has one polished thing that carries the color and the craft, a rendered object, a painted scene or a rich chart. Everything around it is quiet, so it reads instantly. +- **Quiet chrome.** Navigation, buttons and text use the tinted neutrals and one accent. Nothing glows, nothing competes with the bright element. +- **A clear idea.** You can say in one sentence what the visual shows and why it belongs to this product. +- **Text on calm space.** The headline sits on a part of the image or layout that was left empty for it, short and specific, with one primary and one secondary action and nothing stacked under them. +- **Real product, real material.** Previews look like the real app with plausible data, and objects look like real things with weight, light and shadow. + +Which visual styles tend to work, and which fail, is in [references/visuals.md](references/visuals.md). Finished examples to learn from are in [references/refs.md](references/refs.md), and code treatments like drawing an image in glyphs are in [references/effects.md](references/effects.md). + +## Run it + +1. **Look at the screen.** Find the files that render it. If you can run the app or open the file in a browser and take a screenshot, do it, so you judge what people see and not only the code. + - Only a link to a live site, no code? Save the page with `node /scripts/snapshot.mjs unslop/original.html` and work on a copy of that file. Add `--dark` or `--light` to match the theme the user sees. It also saves `original.live.png`, a screenshot of the live page, so render the saved copy and compare the two before you start. Animated backgrounds drawn by scripts do not survive, so describe them in the audit from the live page. + - Only a screenshot? Rebuild the screen as one static HTML file first, matching it closely, and treat that as the original. + - In both cases the result is a redesigned page in code that the user can hand to whoever owns the real site, so say that in the report. +2. **Audit.** Go through [references/signs.md](references/signs.md) and list every sign you find, one line each with where it lives. +3. **Decide how much to change.** Read the audit honestly before planning anything. + - **Heavy slop**, many signs across color, type, icons and copy. Run the full process below. + - **Light slop**, a screen that is mostly designed with a few signs left. Polish mode. Fix only what the audit found, take the direction from the existing brand instead of inventing a new one, and keep the palette, type, layout and visual. Bigger ideas, like a moment that is missing or a scene for the hero, go into the report as suggestions, not changes. + - **No slop.** Say so plainly and suggest two or three small refinements at most, with exact values. Apply them only if the user agrees, and stop there. Never change a good screen just to make it different. +4. **Set the direction and stop.** Write the five lines described below. Show the audit and the direction to the user and wait for their answer before editing, since changing the direction later means redoing everything. If you have a tool for asking the user questions, ask through it, otherwise ask in plain text and stop. Skip the wait only when the user already said to just do it. +5. **Fix in nine passes, in order.** Finish a pass across the whole screen before starting the next. When nearly every line changes, plan the passes in order and then write the new screen in one go, as long as every pass is applied. Spacing comes first because everything else reads differently once the rhythm is right. Copy comes before type and layout because the length of the headline decides its size and where it sits. Visuals and motion come last because they should fit a screen that already works without them. +6. **Check.** Run the checklist at the end of this file, render again and compare with the audit list. Anything still on the list gets fixed. +7. **Report** the direction, a few lines per pass about what changed, and any image prompt you wrote. +8. **Record, when asked or when the user wants something to share.** Build the story file and render the video described in [Story video](#story-video). If the user asks for it at the start, write each pass into the story file as you go. + +Keep a run quick. Render the screen to check it at most three times. When you generate an image, start it right after the direction is confirmed and keep doing the first passes while it runs, instead of waiting for it. + +Exact values for every pass live in [references/tokens.md](references/tokens.md). + +## Direction + +Before touching anything, write five short lines. Derive them from what the page says the product does and who it is for. Never from what is trending. + +- **Product.** What it does and for whom, in one plain sentence. +- **Character.** Two or three concrete words, like `calm, exact, a little warm` or `fast, technical, playful`. Words like clean, modern, sleek, minimal and premium are not allowed, because they describe every template. +- **World.** The place or material the product naturally belongs to. A note app lives near paper and ink, a monitoring tool near instruments and night shifts, a travel app near maps and window light. This world sets the tint of the neutrals, the accent and the visual. +- **Moments.** One or two places where the screen gets rich color, material and detail, chosen from what the product shows. A rendered card or device inside the preview, a chart whose colors each mean a series, an object from the world placed next to the content. Write what they are. +- **Signature.** One small detail only this product would have, used consistently. A live status dot for a monitoring tool, keycaps for a developer tool, hand drawn underlines for a teaching app, a route line for a delivery app. One detail, not five. + +The examples here only show the shape of an answer. Derive yours from the product in front of you, even when an example happens to fit. + +If the product already has a real brand, like a logo file, a color or a typeface that is not slop, keep it and build the direction around it. A violet to blue gradient, sparkles and glow are slop even when they sit in the logo, so they do not count as brand unless the user says they do. + +## 1. Spacing + +- Everything sits on a 4px grid. Margins, paddings, gaps and fixed heights are multiples of 4, so 28 or 104 are fine even though they are not in the token list. A 2px step is fine for nudges inside small controls, and an odd value that exactly centers something stays. +- Same thing, same size. All rows in a list share one height and one gap, all cards in a row share one padding. +- Space shows grouping. Space inside a group is smaller than space between groups, so a list reads as one block and sections read as separate ones. +- Things line up. Icons sit in one column, labels start on one line, numbers align right in one column. Nothing is nudged off the line with a stray translate or odd margin. +- Padding follows density. Compact panels and sidebars around 16, cards 20 to 24, page edges 24 to 32. +- Navigation is the densest part of an app. Rows are 32 to 36 tall with 2 to 4 between them, icons in one column, labels on one line. A section label sits closer to its own items than to the section above it, like 24 above and 8 below. Measure the sidebar after the pass, it is where spacing breaks most often. +- Remove one-off values that break the rhythm, like `mt-[13px]` or `py-[11px]`, and replace them with steps from the scale. + +## 2. Color + +- Build from one neutral ramp, tinted toward the world from the direction. Pick graphite, warm or ink from the tokens, or tint your own the same way. Background, raised surfaces, borders and text all come from that ramp. +- In apps and dashboards the backgrounds, surfaces, borders and text stay neutral, the graphite ramp or something just as grey. People look at them for hours and the content has to carry the color. A screen whose background reads as orange, brown, blue or green has gone too far. The world shows in the accent, the charts and the moments, not in the surfaces. Landing pages can tint their surfaces further toward the world. +- Three text roles, primary, secondary and muted. Not ten slightly different lavenders. +- In the chrome, one accent, used rarely. It marks the primary action, the selection or the one number that matters. +- The moments from the direction are the exception. Inside them color can be rich, several hues in a chart, a rendered object with real materials, an image with its own light, as long as it stays inside the world and each color means something. +- If the brand has a real color, it becomes the accent and leaves the surfaces. If there is no real brand color, take the accent from the world. +- Accent with text on it reaches 4.5 to 1 contrast. If it does not, darken it or put dark text on it. Accent used as text or ink on a light surface needs the same contrast, so use a darker shade of it there. +- On dark themes the primary button can be an off white from the ramp with dark text, and the accent can live in a smaller place like the selection or a key figure. This often looks more deliberate than a colored button. +- Semantic colors only carry meaning, like green for a good change and red for an error, and they are toned down to sit inside the palette. +- Remove hue gradients on surfaces, gradient text, colored glows and colored shadows, radial color washes and blobs behind content, and neon borders. +- Borders are quiet hairlines, the text color of the ramp at 6 to 10% opacity. +- On dark themes depth comes from surface steps and hairlines, not from shadows. A window or object floating over an image may keep a soft neutral shadow. On light themes shadows are neutral, soft and small. +- A gradient is allowed only as a small tone shift inside one neutral, like an active row going from #2b2b2b to #1f1f1f. Never from one hue to another. + +## 3. Icons + +- Every emoji used as an icon or as decoration in a label becomes a line icon from one set. Use the set the project already has, otherwise lucide. +- One size and one stroke everywhere, 16 or 18px with a 1.5 stroke. +- Icons take the color of their text role, usually secondary or muted. They never get their own colors. +- The icon box has the same width in every row and the gap to the label is fixed, 8 or 10, so labels align. +- Drop icons that add nothing, like an emoji in every card title or before every section label. Categories that need color get a small status dot. + +## 4. Copy + +- Headlines say what the product does for whom, in plain words, so they could not fit another product. Use the facts already on the page and do not invent new ones. +- No exclamation marks in interface text, and never `!!`. +- No greeting headlines like `Welcome back, Damir! 👋`. The title names the context, like the page name or the period, for example `October 2026`. +- Short labels. `Search`, not `Search anything...`. +- A change says what it compares to, like `+12% vs last month` or `3 fewer vs last month`. +- Headlines, subheads, labels, buttons and captions end without a period. Only paragraphs of several sentences keep their periods. +- No hype words like supercharge, seamless, unlock, magic, revolutionize or game-changing, and no `AI-powered` on everything. +- Claims that cannot be checked, like `Loved by 10,000+ teams` or `10x faster`, become quiet and specific if the user confirms they are real, and are removed otherwise. Ask, and list them in the report. +- Mock data looks real, with plausible numbers and names, never `[X]` or `123,456`. +- Badges like `PRO`, `New ✨` or `Beta 🚀` go away unless the product really needs them, and then they are quiet text. + +## 5. Type + +- Choose type for the character in the direction, from the families in the tokens. Keep the project's font when it is part of a real brand. A default like Inter that nobody chose may be swapped when another family fits the character better. One family for the interface, plus at most one partner from any row of the tokens, a mono for figures and meta or a display face for large headlines. +- A few sizes from a scale and two weights, 400 and 500, with 600 at most for rare emphasis. No 700 to 900 headings, no gradient text. +- Hierarchy comes from size and color, not from weight. +- Sentence case everywhere. Labels like `NAVIGATION` or `PAID THIS MONTH` with wide tracking become `Navigation` and `Paid this month`. +- Small text never gets negative letter spacing. Readability on small text means bigger, a bit bolder or slightly looser. Tight tracking is only for large display text. +- Numbers use `tabular-nums` and one format across the screen. If one figure is `$48,210`, the next is not `$124,860.40`. +- No italic for emphasis. + +## 6. Layout + +- Keep the sections and their order. Inside a section, alignment and proportions may change when that makes the screen clearer. +- Apps and dashboards keep real app proportions. The shell fills the viewport, a sidebar of 240 to 280 and fluid content, usually capped around 1440 to 1600 wide, and the page is as long as its content, with more sections below the first screen. Never shrink an app into a narrow centered column. If the original was drawn as a small window on a backdrop, keep it visibly a window, with its frame and backdrop, and still give it desktop proportions inside. +- The strongest landing hero is a scene. One generated image fills the hero from edge to edge, its subject sits on one side, and the headline sits on the calm empty part of the same image, with the product view rising from the bottom edge. Look at the hero in the references to see it. +- Without a scene, use an asymmetric split, text on one side and the visual given at least half the width, or text over a large product view below it. Everything stacked and centered is the template default, keep it only for a short hero with no visual. +- In a scene hero, the text block sits in the upper part, in the space between the navigation and the top of the product view, at its optical center, a little above the true middle. +- In a split, center the text block vertically against the visual, so neither side leaves a big empty corner. If the visual is much taller, shorten it instead of pushing the text down. Floating cards and devices next to the text are hard to balance, prefer one solid visual. The spacing token from the navigation to the headline applies to stacked heroes, a split hero is centered instead. +- Long headlines align left. Centered text works for one or two short lines. +- One primary action and at most one secondary action next to it. Anything else moves out of the hero. +- Navigation stays quiet. Logo, the existing links, one button. Log in is a quiet text link next to it, not a second button. A floating pill header is not required. On small screens the links collapse into one menu button. +- Compose the first screen as a whole. The hero fills the viewport height without a dead empty band. If the content is shorter, center it or let the product view start low and run past the bottom edge, so the page invites a scroll. +- On small screens everything stacks in reading order, the headline drops to the mobile size from the tokens and the visual comes after the actions. A dense preview gets simpler on mobile, with fewer rows and columns, instead of being zoomed down. No text in it goes below 11px. + +## 7. Details + +- Nested radius. An inner radius equals the outer radius minus the padding between them, never less than 4. Controls 8 to 10, cards 12 to 16, panels 14 to 20. Not everything is a pill. +- Counts and meta become quiet text, often mono and muted, instead of glowing pills. +- Keyboard shortcuts become small keycaps. +- Charts sit on a muted grid with muted mono labels. No rainbow bars inside one series, no glowing bars, no uppercase day labels. If the chart is placeholder noise, replace it with data that tells something real. +- Charts have a hierarchy, like text. One main chart is clearly the biggest thing on the screen, drawn with care, like smooth lines or a mirrored comparison. Anything else that shows data is smaller and different in kind, like a ranked list with thin bars or a short table, never a second chart of the same size. +- One accent per chart. The thing that matters, like the current month, the line you are tracking or the plan that grew, gets the accent, and everything else sits in neutral steps of the ramp. Several hues only when the chart really has to tell series apart, and even then one leads and the rest are quiet. +- Stat cards are numbers. A label, a big figure and a change with what it compares to. No sparklines, tallies, mini bars or icons added to fill them, they read as decoration. A card can hold a real looking object when it is the very thing its number counts, like a paid receipt next to paid revenue or bank cards next to spend. Use as many as make sense for that dashboard, as long as each one reads on its own and none of them is a mini chart. +- Vary the sizes. Cards of equal width side by side, each with the same kind of content, read as a template. Give the grid a rhythm, like three or four compact stats in a row above one wide chart, or a wide chart next to a narrow list. +- Pick the chart type for the data. A trend is a line, a comparison is bars, a share is one stacked bar or a list with bars, a single goal is a progress bar. +- The active state is a small surface step with a hairline highlight, not a gradient with a glow. +- Hover and focus states exist and stay inside the palette. A visible focus ring for keyboard users. +- The logo is the product name in the main family, plus a simple mark if there is one. No gradient wordmark and no sparkle tile. +- The signature detail from the direction appears where it belongs and nowhere else. + +## 8. Visuals + +- Decorative filler goes. Glossy blobs, orbs, nebulas, floating cubes, glowing grids, holographic brains and robots say nothing about the product. +- A landing hero gets a visual that shows something true. +- Dashboards and app screens almost never need generated images. Their moments come from the product itself: a chart drawn with care, figures set beautifully, real content like product photos or avatars, a status that feels alive. A physical thing the product deals with, like a payment card in a finance app, is usually better built in code with real materials, a soft gradient, a chip, a number. +- Generate images for an app only for empty states, onboarding and marketing moments, or when the user asks for it. +- With an image generation tool, the first choice for a landing hero is a scene from the world in the direction, generated wide enough to sit behind the whole hero, with calm empty space where the text goes. It carries the color and the mood, and the chrome stays quiet over it. +- Under the scene, or as the visual when there is no tool, show the product. Build a preview in code of what the page already promises, with the project's components or careful HTML and plausible data, like the main screen, one key card or the result the product produces. Showing what the copy already claims is not inventing a feature. In a scene hero it rises from the bottom edge and runs past it. +- The product view carries a moment, not just a plain card. A rendered object inside it, like a payment card on a figure, a rich chart or a small object from the world. These are kinds of objects, so using one that fits is fine. +- Before this pass, look at the references in [references/refs.md](references/refs.md). Take qualities from them, like composition, light, restraint and how the moment is placed, never their subject or layout. +- Rendered objects and scenes come from an image generation tool when you have one. If you do not, build what you can in code, write the prompts for the rest and leave empty slots of the right size. +- How to pick the subject, the prompt recipe and how to place the image are in [references/visuals.md](references/visuals.md). Read it before this pass. + +## 9. Motion + +- Remove motion that loops for no reason, like `animate-pulse` or `animate-ping` on things that are not live, shimmer on headings, breathing glows, floating elements and parallax. +- Remove `transition-all`, bouncy springs, `hover:scale` on cards and anything that moves more than a few pixels on hover. +- Keep motion that explains something. A hover is a quick color or surface change, a press is a tiny scale down, a menu fades and rises a few pixels from where it opens, a changed number tweens. +- Transition color, background, border, opacity and transform, and name them, like `transition-colors` or `transition-[transform,opacity]` in Tailwind, instead of a catch all. Never animate width, height, top, left, margins or large blurs, they stutter on weak devices. +- One easing curve and a few durations across the screen, from the tokens. Short for feedback, a little longer for things that enter. +- At most one entrance on load, a short fade and rise for the main content with a small stagger. Not every card flying in from a different side. + +## Story video + +A short video where the screen unslops itself step by step, with red redlines on the slop. Look at [refs/story-dashboard-frames.webp](references/refs/story-dashboard-frames.webp) before you build it, eight frames from it in order. That video went viral, and this is how it works: + +- **Nothing is swapped, everything morphs.** One page, and each step only changes CSS, so padding, gaps, sizes, colors, radii and shadows tween from the old value to the new one in place. You see the spacing open up, the purple drain out, the corners soften. +- **A wave.** Elements start a few dozen milliseconds apart from the top left down, so a change runs across the screen instead of everything flipping at once. +- **Text rolls.** Copy that changes blurs out, and the new words rise in letter by letter. +- **Redlines first.** The slop gets red selection boxes with tags, a spacing step shows red measurements that turn white once fixed, a details step outlines corners with their radius. +- **Close and short.** Square, cropped close on the part that changes most, so it reads on a phone, and about 10 seconds. + +It adds a few minutes, so make it only when the user wants it. + +1. **One story file.** Build `unslop-story/story.html`, a standalone copy of the original screen with its styles in the file. Keep the original rules as they are, they are step 0. +2. **Steps.** Three to five, merging passes so each one changes something you can see, like `Spacing`, `Color and icons`, `Type and copy`, `Details`. A new chart or visual goes into the last step. List them in the head as ``. A name containing Spacing shows spacing measurements, one containing Details shows corner outlines. +3. **Each step is CSS on top.** Write the changes of step 1 under `.s1`, step 2 under `.s2` and so on, like `.s1 .card { padding: 24px }` or `.s2 body { background-color: #141415 }`. The classes add up, so step 3 still has the rules of steps 1 and 2. Change values, never structure, so the browser can tween them. +4. **What cannot tween, and how to handle it.** + - Gradients, glows and washes: put them on their own layer and fade its `opacity` to 0, with the solid color underneath changing. Gradient text goes on the `.from` span only, never on the swap or its parent, because a text clip also catches the hidden new text. + - Pills: write their radius as half the height, like `23px`, not `9999px`, or the corner only changes in the last frame. + - Use swaps sparingly, only where the words change, at most eight in a story. Twenty swaps blurring at once read as a cut between two pictures, which is exactly what the video must not look like. Weight, size, tracking and color changes tween on their own, so they never need a swap. A new font family switches directly in its step, without swaps. + - Text that changes, a new font family, an emoji that becomes an icon: `Welcome back, Damir! 👋October 2026`. At step 3 the old part blurs out and the new one rolls in. + - Emoji that become icons: put the new icon next to the emoji inside the same box, both absolutely stacked, and crossfade their `opacity` in the step. That is not a swap and does not count toward the limit. + - Things that disappear, like a `New ✨` badge: `data-out="2"` collapses them at step 2. + - Things that appear, like a legend: `data-in="4"` fades them in at step 4. Reserve their space so nothing jumps. +5. **Redlines.** The measurements follow the elements live, so their numbers count from the old value to the new one while things move. That is the moment people watch, so choose them well. + - `data-gap` goes on the blocks of a vertical stack, like the brand row, the search, the navigation group and the plans group in a sidebar, or the headline, the stat row and the chart in the main column. Not on every row, or the screen fills with tiny 2px tags. Aim for four to six measured gaps per stack, and give each stack its own name if there is more than one, like `data-gap="sidebar"` and `data-gap="main"`. + - `data-radius` goes on two to four buttons and cards whose corners change. + - `data-slop="Gradient text"` and so on marks the slop for a short first beat, at most five, on the clearest examples that sit apart from each other, with short tags. It is optional, the reference video has none. +6. **Only the screen and its background.** The video shows the story page as it is, with no extra window, border or shadow around it, so the page's own background fills the frame and the screen is the only box. If the screen sits in a window that grows to desktop proportions, anchor it to the left in the story so it widens to the right instead of gliding sideways. The story page renders 1440 wide and at least 900 tall, so do not size things from `100vh`, and keep `position: fixed` for background layers only. +7. **Camera.** `` sets which part of the page fills the frame, as x, y and width in page pixels. Point x and y at the screen's own top left corner, so the frame starts with the page background around it, and crop close on where the most visible change happens, usually the sidebar or headline and the first cards. +8. **Render.** `node /scripts/story.mjs unslop-story/story.html unslop-story.mp4`, square by default, `--wide` for 16:9. It needs Playwright and ffmpeg, so if either is missing, tell the user the one command that installs it. To watch it live first, run it with `--play` instead of the output path, and it loops in a browser window until closed. Pull a few frames with ffmpeg and look at them before you hand it over. Checking the story file and the video does not count toward the render limit of a run. +9. **The story file is a presentation piece.** The clean result still ships as normal code in the project's own stack, without `.s` classes or story attributes. Every video carries a small `unslop by @heydamir` credit, as the license asks. + +## Do not + +- Do not rename the product, even when the name has AI in it. Keep a real logo as it is. When the logo itself is slop, like an emoji or a sparkle in a gradient tile, redraw it flat: the wordmark alone in the new type, or a simple glyph from the product's idea in one solid color. Never a glossy or gradient ball, a glow or a gradient tile, those read as AI at first sight. +- Do not change what the product is. Keep its features, content and the order of sections unless the user asks for more. Placeholder content that means nothing may be replaced with content that does. +- Do not replace removed decoration with new decoration, and do not swap one trend for another like glass, grain, noise, neon or bento grids. +- Do not invent features, claims or data. +- Do not fall back to the same look for every product. The direction decides the palette tint, type and visual, not habit. + +## Checklist + +- [ ] The direction is written and every pass follows it +- [ ] Every spacing value is on the 4px scale and equal things have equal sizes +- [ ] Icons, labels and numbers each sit in one aligned column +- [ ] One tinted neutral ramp, three text roles, one accent with enough contrast +- [ ] No hue gradients, gradient text, glows, colored shadows or color blobs +- [ ] No emoji left in the interface +- [ ] One icon set at one size and stroke +- [ ] Type chosen for the character, weights 400 to 600, no uppercase tracked labels, no italic, no negative tracking on small text +- [ ] No exclamation marks, no trailing periods on short strings, no greeting headline, no unverified claims +- [ ] Numbers in one format with tabular figures +- [ ] Nested radii add up +- [ ] A hero has a real visual or a product preview with at least one moment, never filler and never a labeled placeholder +- [ ] The first screen is composed as a whole with no dead empty band +- [ ] No looping or bouncy motion, one easing curve, no animated layout properties +- [ ] Template check. If you swapped the product name for another product's, would the screen still fit? If yes, push the direction harder in the copy, the visual and the signature detail diff --git a/vendor/interface-design/heydamir-unslop/skills/unslop/agents/openai.yaml b/vendor/interface-design/heydamir-unslop/skills/unslop/agents/openai.yaml new file mode 100644 index 0000000..cfecf69 --- /dev/null +++ b/vendor/interface-design/heydamir-unslop/skills/unslop/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Unslop" + short_description: "Turn vibecoded, AI-looking UI into a designed interface" + default_prompt: "Use $unslop on this screen so it stops looking AI-generated." diff --git a/vendor/interface-design/heydamir-unslop/skills/unslop/references/effects.md b/vendor/interface-design/heydamir-unslop/skills/unslop/references/effects.md new file mode 100644 index 0000000..621f8ec --- /dev/null +++ b/vendor/interface-design/heydamir-unslop/skills/unslop/references/effects.md @@ -0,0 +1,113 @@ +# Effects + +Code treatments that turn an image into something made for this page. They are optional. Use one only when it fits the character in the direction, on the main visual only, and never as decoration on other elements. + +## Glyphs + +Draw the image as a grid of characters, brightness picking the character. It works best when the characters come from the product's own material, so the image is literally made of what the product deals with. + +- Money, invoices, finance: `. : 1 7 4 0 8 $ %` +- Code, developer tools: `. : / { } < > = #` +- Music: note names like `c d e g a` and `♭ ♯` +- Writing, reading: lowercase letters of a word from the product +- Data, monitoring: `. · : | + ▪` + +Keep the cell between 6 and 10px, use the mono from the type choice, and color the glyphs from the image or from the ramp. Let a few cells change slowly over time so the picture feels alive. + +### Implementation + +```js +function glyphs(canvas, image, { cell = 8, chars = ". :1740$%", font = "Geist Mono", fps = 8 } = {}) { + const ctx = canvas.getContext("2d"); + const columns = Math.floor(canvas.width / cell); + const rows = Math.floor(canvas.height / cell); + + const sample = document.createElement("canvas"); + sample.width = columns; + sample.height = rows; + const sampler = sample.getContext("2d", { willReadFrequently: true }); + sampler.drawImage(image, 0, 0, columns, rows); + const pixels = sampler.getImageData(0, 0, columns, rows).data; + + const atlas = document.createElement("canvas"); + atlas.width = cell * chars.length; + atlas.height = cell; + const pen = atlas.getContext("2d"); + pen.font = `500 ${cell + 1}px ${font}`; + pen.textAlign = "center"; + pen.textBaseline = "middle"; + pen.fillStyle = "#fff"; + [...chars].forEach((char, index) => pen.fillText(char, index * cell + cell / 2, cell / 2)); + + const cells = []; + for (let y = 0; y < rows; y++) { + for (let x = 0; x < columns; x++) { + const i = (y * columns + x) * 4; + const light = (0.2126 * pixels[i] + 0.7152 * pixels[i + 1] + 0.0722 * pixels[i + 2]) / 255; + cells.push({ x, y, light, color: `rgb(${pixels[i]},${pixels[i + 1]},${pixels[i + 2]})`, seed: Math.random() }); + } + } + + const tinted = document.createElement("canvas"); + tinted.width = cell; + tinted.height = cell; + const tint = tinted.getContext("2d"); + + const draw = (step) => { + ctx.clearRect(0, 0, canvas.width, canvas.height); + for (const c of cells) { + if (c.light < 0.06) continue; + const jitter = Math.sin(c.seed * 999 + step) > 0.92 ? 1 : 0; + const pick = Math.min(chars.length - 1, Math.floor(c.light * chars.length) + jitter); + tint.globalCompositeOperation = "copy"; + tint.drawImage(atlas, pick * cell, 0, cell, cell, 0, 0, cell, cell); + tint.globalCompositeOperation = "source-in"; + tint.fillStyle = c.color; + tint.fillRect(0, 0, cell, cell); + ctx.drawImage(tinted, c.x * cell, c.y * cell); + } + }; + + let frame = 0; + let last = 0; + let step = 0; + let visible = true; + new IntersectionObserver(([entry]) => (visible = entry.isIntersecting)).observe(canvas); + const loop = (now) => { + frame = requestAnimationFrame(loop); + if (!visible || now - last < 1000 / fps) return; + last = now; + draw(step++); + }; + frame = requestAnimationFrame(loop); + return () => cancelAnimationFrame(frame); +} +``` + +For a large canvas, bake the tinted glyphs for each cell once into an offscreen canvas and only redraw the cells that change on a step. + +## Halftone dots + +Dots on a grid whose size follows brightness. Suits editorial, print and crafted characters. Same sampling as glyphs, but draw `arc` circles with a radius of `light * cell / 2` in one ink color on the page color. + +## Dithering + +An ordered or error diffusion dither into two or three colors from the ramp. Suits retro, technical and playful characters. Do it once when the image loads, not on every frame. + +## Duotone + +Map the image's brightness onto two colors, the darkest surface of the ramp and the accent or the primary text color. The simplest way to make any generated image belong to the palette. In CSS, a grayscale image under a `mix-blend-mode: color` layer of the accent, or an SVG `feColorMatrix` filter. + +## Performance + +These effects broke on weak laptops before, so keep to these rules. + +- Read pixels once when the image loads. Never call `getImageData` inside the animation loop, it stalls the GPU and drops frames to single digits. +- Prepare glyph atlases and tinted cells ahead of time and draw them with `drawImage`. +- Animate at 8 to 12 steps per second, not 60, and change only a small share of cells per step. +- Pause when the canvas is off screen. +- Size the canvas for the device pixel ratio, but cap it at 2. + +--- + +Part of unslop by Damir Sokolovsky ([@heydamir](https://x.com/heydamir)), CC BY 4.0. Keep this credit in copies and adaptations. diff --git a/vendor/interface-design/heydamir-unslop/skills/unslop/references/refs.md b/vendor/interface-design/heydamir-unslop/skills/unslop/references/refs.md new file mode 100644 index 0000000..e6c23aa --- /dev/null +++ b/vendor/interface-design/heydamir-unslop/skills/unslop/references/refs.md @@ -0,0 +1,17 @@ +# References + +Finished screens by Damir Sokolovsky ([@heydamir](https://x.com/heydamir)). Look at them before the Visuals pass. Take their qualities, never their subject, palette or layout, so every product still gets its own screen. `unslop-dashboard-after` is the finished version of `examples/dashboard.html`, so when you unslop that example, find your own answer instead of rebuilding it. + +| File | What it shows | Why it works | +| --- | --- | --- | +| [unslop-hero-after.webp](refs/unslop-hero-after.webp) | An invoicing hero with a paper plane drawn in digits and currency signs, small notes pinned to it, and the product rising from below | The visual is made of the product's own material, money, so it is the moment and the signature at once. Text sits on calm sky, the chrome is quiet, and the product view invites a scroll | +| [unslop-dashboard-after.webp](refs/unslop-dashboard-after.webp) | A finance dashboard with two rendered payment cards and a revenue chart drawn as soft colored curves over a mirrored grey comparison | Quiet graphite chrome, and all the color lives in two moments, a real looking object and a chart where every color means a series | +| [beacon-engraving.webp](refs/beacon-engraving.webp) | An incident monitoring hero drawn as one blue engraving of a lighthouse on a rocky coast, with a serif headline in the same ink | A print style working because the whole page is in it. One ink, one paper tone, and the headline sits in the calm sky the drawing leaves for it | +| [meadow-3d.webp](refs/meadow-3d.webp) | A footer as a soft 3D meadow with a few matte crystal shapes and a huge wordmark standing behind the hills | Friendly stylized 3D with a restrained palette, type that becomes part of the scene, and quiet links on the open sky | +| [story-dashboard-frames.webp](refs/story-dashboard-frames.webp) | Eight frames from the viral story video, a vibecoded dashboard unslopping itself in about 10 seconds | Everything morphs in place in a wave, red redlines measure the slop and turn white when fixed, copy rolls letter by letter, and the frame is square and cropped close on the sidebar and first cards | + +What they share: one bright element per screen, quiet chrome around it, a visual that can be explained in one sentence, and headlines placed on space that was left empty for them. + +--- + +Part of unslop by Damir Sokolovsky ([@heydamir](https://x.com/heydamir)), CC BY 4.0. Keep this credit in copies and adaptations. diff --git a/vendor/interface-design/heydamir-unslop/skills/unslop/references/refs/beacon-engraving.webp b/vendor/interface-design/heydamir-unslop/skills/unslop/references/refs/beacon-engraving.webp new file mode 100644 index 0000000..cf8af41 Binary files /dev/null and b/vendor/interface-design/heydamir-unslop/skills/unslop/references/refs/beacon-engraving.webp differ diff --git a/vendor/interface-design/heydamir-unslop/skills/unslop/references/refs/meadow-3d.webp b/vendor/interface-design/heydamir-unslop/skills/unslop/references/refs/meadow-3d.webp new file mode 100644 index 0000000..d8284c8 Binary files /dev/null and b/vendor/interface-design/heydamir-unslop/skills/unslop/references/refs/meadow-3d.webp differ diff --git a/vendor/interface-design/heydamir-unslop/skills/unslop/references/refs/story-dashboard-frames.webp b/vendor/interface-design/heydamir-unslop/skills/unslop/references/refs/story-dashboard-frames.webp new file mode 100644 index 0000000..b9a0872 Binary files /dev/null and b/vendor/interface-design/heydamir-unslop/skills/unslop/references/refs/story-dashboard-frames.webp differ diff --git a/vendor/interface-design/heydamir-unslop/skills/unslop/references/refs/unslop-dashboard-after.webp b/vendor/interface-design/heydamir-unslop/skills/unslop/references/refs/unslop-dashboard-after.webp new file mode 100644 index 0000000..9836639 Binary files /dev/null and b/vendor/interface-design/heydamir-unslop/skills/unslop/references/refs/unslop-dashboard-after.webp differ diff --git a/vendor/interface-design/heydamir-unslop/skills/unslop/references/refs/unslop-hero-after.webp b/vendor/interface-design/heydamir-unslop/skills/unslop/references/refs/unslop-hero-after.webp new file mode 100644 index 0000000..ad90593 Binary files /dev/null and b/vendor/interface-design/heydamir-unslop/skills/unslop/references/refs/unslop-hero-after.webp differ diff --git a/vendor/interface-design/heydamir-unslop/skills/unslop/references/signs.md b/vendor/interface-design/heydamir-unslop/skills/unslop/references/signs.md new file mode 100644 index 0000000..e9bae03 --- /dev/null +++ b/vendor/interface-design/heydamir-unslop/skills/unslop/references/signs.md @@ -0,0 +1,103 @@ +# Signs of slop + +Go through every group and note each sign you find with where it lives. Each line pairs the sign with its fix. + +## Surfaces and color + +- Purple to blue or violet to pink gradients on backgrounds, cards, buttons or the active nav item → one neutral surface, accent only where it means something +- Gradient text in headings, numbers or the logo → solid primary text color +- Colored glows, like `shadow-[0_0_20px_rgba(139,92,246,0.5)]`, `drop-shadow` on emoji or glowing pills → remove, depth from surface steps and hairlines +- Radial color washes or blurred blobs behind content → remove +- Neon or high contrast colored borders → hairline at 6 to 10% opacity +- Many near identical tints of the same hue for text, like five lavenders → three text roles from one neutral ramp +- Every element highlighted, so nothing stands out → one accent used once or twice per screen +- Glass cards with a white inner highlight and a colored shadow → plain raised surface with a hairline + +## Type + +- Headings at weight 800 or 900 → 400 to 500 and size for hierarchy +- Uppercase labels with wide tracking, like `NAVIGATION` or `PAID THIS MONTH` → sentence case at normal or slightly open tracking +- Too many sizes and weights on one screen → a few sizes from the scale, weights 400 and 500 +- Numbers in mixed formats, like `$48,210` next to `$124,860.40`, or `07` → one format, tabular figures +- Negative letter spacing on small text → zero or slightly positive +- Italic used for emphasis → color or weight instead + +## Icons + +- Emoji as nav icons, label decoration or status, like 🏠 📥 🚀 💰 ⚠️ 📈 🔥 😬 → one line icon set or nothing +- Emoji inside pills and counts, like `18 👤` → plain number +- Icons in many sizes and colors → one size, one stroke, text role color +- Icons nudged off the column → same icon box width in every row +- A sparkle ✨ for anything AI → remove or use a plain icon only where it explains something + +## Spacing + +- Arbitrary values, like `mt-[13px]`, `ml-[7px]`, `h-[31px]` → steps on the 4px scale +- Rows in one list with different heights or gaps → one height, one gap +- Groups that sit closer to their neighbours than to their own items → smaller gaps inside groups, bigger between them +- Uneven padding inside sibling cards → one padding +- Big empty gaps above a section and cramped space below it → balanced rhythm + +## Copy + +- Greeting headlines, like `Welcome back, Damir! 👋` → the page or period as the title +- Exclamation marks and `!!` → plain statement +- Changes without a reference, like `+12%!!` or `3 less!!` → `+12% vs last month`, `3 fewer vs last month` +- Wordy placeholders, like `Search anything...` → `Search` +- Hype words, like supercharge, seamless, unlock, magic, revolutionize → what the thing actually does +- Trailing periods on labels and buttons → none +- Fake looking data, like `$123,456` or `[X]` → plausible numbers + +## Components + +- Glowing pills for counts, plans, shortcuts or status → quiet mono text or a keycap +- `PRO`, `New ✨`, `Beta 🚀` badges everywhere → remove or one quiet text badge +- Every element a full pill, from search to buttons to cards → radius by size, nested radius math +- Charts with rainbow bars, glowing bars, pill shaped bars or uppercase day labels → one or two series, muted grid and mono labels, one highlight color +- Every chart or stack in several shades of the accent, so nothing leads → neutral steps with the accent only on the one thing that matters +- Sparklines, tallies or mini bars added to every stat card → the number, its label and its change +- Equal cards side by side, each with the same chart or layout → one main chart, compact stats, varied widths +- Logo as a gradient tile with a sparkle and a gradient wordmark → product name as text plus a simple flat mark +- Logo mark as a glossy orb, a gradient ball or a glowing dot → a flat glyph in one solid color, or the wordmark alone + +## Landing pages and heroes + +- A pill badge above the headline, like `✨ Introducing X 2.0` → remove, or a quiet text link if it is real news +- A headline that could fit any product, like `Supercharge your workflow with AI` → say what the product does for whom +- Gradient or shimmer on the headline → solid text +- Floating blobs, orbs, grids and glowing lines behind the hero → plain background or one real product visual +- Everything stacked and centered, headline, text, buttons and a picture below → an asymmetric split or text over a large product view, see the Layout pass +- Three or more buttons, or chips and avatars stacked under the buttons → one primary and at most one secondary action +- `Trusted by 10,000+ teams`, `Loved by 10,000+`, rows of avatar dots or five stars without a source → ask if it is real, then make it quiet and specific or remove it +- Parallax and elements floating on scroll → static layout, motion only with a purpose +- Feature cards with an emoji, a bold title and a vague line each → fewer features, concrete copy, line icons or none + +## Visuals + +- Glossy 3D blobs, orbs, liquid chrome shapes or a purple nebula as the hero visual → the product itself or one concrete metaphor for what it does +- Stock robots, glowing brains, circuit lines or holograms for anything AI → remove, or show what the AI actually produces +- Text, fake buttons or a fake logo baked into an image → real text in the page, a clean image without text +- An image that does not blend into the page, with a hard edge and a different black → background matched to the page and a soft mask at the edges +- A visual on every section → one visual where it helps, usually only the hero + +## Motion + +- `animate-pulse`, `animate-ping` or breathing glows on things that are not live → static +- `transition-all` and `hover:scale` on cards and buttons → color or surface step on hover, a tiny scale down on press +- Bouncy springs and overshoot on simple UI → one ease-out curve +- Everything flying in on load from different directions → one short fade and rise with a small stagger +- Parallax, floating loops and elements that move on scroll → static layout +- Animating width, height, top or left → transform and opacity + +## Template look + +These are not slop, but they are what a cleaned up screen falls back to when nobody made a decision. Watch for them after the passes. + +- Pure grey #161616 page, Inter, white text and the old brand color on one button, on a product whose world is warm or bright → a ramp tinted toward the world and an accent from it +- A headline that would fit another product if you swapped the name → say what this product does, with its own nouns +- A hero visual that is an empty box or a generic picture → a product preview in code or an image from the world +- Every product getting the same layout, type and palette → let the direction choose + +--- + +Part of unslop by Damir Sokolovsky ([@heydamir](https://x.com/heydamir)), CC BY 4.0. Keep this credit in copies and adaptations. diff --git a/vendor/interface-design/heydamir-unslop/skills/unslop/references/tokens.md b/vendor/interface-design/heydamir-unslop/skills/unslop/references/tokens.md new file mode 100644 index 0000000..431cdc3 --- /dev/null +++ b/vendor/interface-design/heydamir-unslop/skills/unslop/references/tokens.md @@ -0,0 +1,135 @@ +# Tokens + +Starting values for each pass. When the project already has a good system, keep its values and only fix how they are used. These are starting points, not a house style, so let the direction move them. + +## Spacing scale + +4, 8, 12, 16, 20, 24, 32, 40, 48, 64, 80, 96, 128 + +- Inside a control: 4 to 12 +- Between related items, like rows in a list: 2 to 8 +- Between groups, like a label and its list or two cards: 12 to 24 +- Between sections: 32 to 64, landing sections 80 to 128 +- Row heights: 32 compact, 36 default, 40 to 44 touch +- Hero: 96 to 128 from the navigation to the headline on desktop, 48 to 64 on mobile + +## App layout + +- Sidebar 240 to 280 wide, content fluid with page padding 24 to 32, capped around 1440 to 1600. +- Top bar or page header 56 to 64 tall. +- Navigation rows 32 to 36, gap 2 to 4, section labels 24 above and 8 below. +- Stat cards in a row share one height, usually 3 or 4 compact cards per row on desktop, numbers only. +- The main chart spans at least two thirds of the content width and is 280 to 360 tall. A second data block next to it is a list or table, narrower, not another chart. + +## Neutral ramps + +For apps and dashboards use graphite, or a ramp just as grey, for every surface. For landing pages pick the ramp whose tint matches the world in the direction. Warm suits paper, wood, food, craft and anything human. Ink suits night, sky, water and anything technical. You can tint your own ramp the same way, keeping saturation very low, so a surface never reads as a color on its own. + +### Dark + +| Role | Graphite | Warm | Ink | +| --- | --- | --- | --- | +| Deepest surface, like a sidebar | #0f0f10 | #12110f | #07080d | +| Page background | #141415 | #161513 | #0b0d14 | +| Card | #19191b | #1c1a18 | #11141c | +| Raised, like an active row or input | #232325 | #262421 | #1a1e28 | +| Hairline | primary text at 8% | primary text at 8% | primary text at 8% | +| Primary text | #ededed | #efe9de | #eef0f5 | +| Secondary text | #a1a1a6 | #aaa398 | #a3a9b8 | +| Muted text and icons | #85858b | #8a847a | #7a8296 | + +### Light + +| Role | Graphite | Warm | Ink | +| --- | --- | --- | --- | +| Page background | #fafafa | #f7f4ee | #f5f7fa | +| Card | #ffffff | #fdfbf7 | #ffffff | +| Raised, like an active row or input | #f4f4f5 | #efe9df | #eceff4 | +| Hairline | primary text at 8% | primary text at 8% | primary text at 8% | +| Primary text | #171717 | #1f1b16 | #111827 | +| Secondary text | #525252 | #5e554a | #4b5565 | +| Muted text and icons | #737373 | #7b7266 | #6b7487 | + +Muted text is for meta and captions only and stays near 4.5 to 1 on its surface. Body text uses primary or secondary. + +Semantic colors, toned to sit inside any ramp: positive #4cc38a on dark and #16a34a on light, negative #e5484d on dark and #dc2626 on light. + +## Accent + +- Keep a real brand color. A violet to blue or any other hue to hue glow gradient is slop, not a brand color, unless the user says otherwise. +- Without a brand color, take it from the world. Warm worlds suit amber, coral or ember orange. Cool worlds suit cobalt, teal or a clear sky blue. Natural worlds suit moss or sage. Avoid the purple to blue family unless the brand really is purple. +- Text on the accent reaches 4.5 to 1 contrast. Darken the accent or use dark text when it does not. +- The accent covers a few percent of the screen, like the primary action, the selection and one key figure. + +## Buttons + +- Hero buttons 44 to 48 tall, navigation buttons 32 to 36, padding 16 to 20 on the sides. +- Primary on dark: an off white from the ramp with dark text, or the accent. Primary on light: the primary text color as a fill with white text, or the accent. +- Secondary: the raised surface with a hairline, primary text. +- A soft top highlight like `inset 0 1px 0 rgba(255,255,255,0.12)` on dark buttons gives depth without a glow. + +## Type families + +Pick by the character in the direction. All of these are free, and a partner can come from any row. + +| Character | Interface family | Optional partner | +| --- | --- | --- | +| Precise, technical, calm | Geist, Inter, IBM Plex Sans | Geist Mono, IBM Plex Mono, JetBrains Mono for figures and meta | +| Warm, friendly, human | Instrument Sans, DM Sans, Figtree | Instrument Serif or Newsreader for the hero headline only | +| Editorial, literary, crafted | Inter or Instrument Sans for the interface | Newsreader, Instrument Serif or Fraunces for headlines | +| Playful, bold | Space Grotesk, Bricolage Grotesque | a mono for small labels | + +A display serif belongs to large headlines only, never to labels or buttons. + +## Type scale + +| Use | Size | Line height | Weight | +| --- | --- | --- | --- | +| Small labels, meta, counts | 12 | 16 | 400, mono welcome | +| Secondary text, captions | 13 to 14 | 20 | 400 | +| Body, nav items | 14 to 15 | 20 to 22 | 400 to 500 | +| Logo wordmark | 16 to 18 | 1 | 500 to 600 | +| Hero subhead | 18 to 20 | 28 to 30 | 400, secondary color, at most 36rem wide | +| Card figures | 28 to 32 | 1 | 500, tabular | +| Page title | 28 to 36 | 1.15 | 400 to 500 | +| Hero headline | 56 to 72 desktop, 36 to 44 mobile | 1.02 to 1.08 | 400 to 500 | + +Letter spacing is 0 for anything under 16px, slightly positive like 0.01em is fine for small muted text. Display text from 40px can go to -0.02em, and from 56px to -0.03em. + +## Radius + +| Element | Radius | +| --- | --- | +| Keycap, tag, small button | 4 to 6 | +| Row, input, button | 8 to 10 | +| Card | 12 to 16 | +| Panel or window | 14 to 20 | + +Nested radius: inner = outer − padding, never below 4. A 16px card with 8px padding holds items with an 8px radius. + +## Icons + +lucide, 16 or 18px, stroke 1.5, color of the muted or secondary text role, 8 to 10px gap to the label. + +## Depth + +- Dark: surface steps plus a hairline. An active row can carry `inset 0 1px 0 rgba(255,255,255,0.12)` as a top highlight. +- Light: `0 1px 2px rgba(0,0,0,0.05)` for small things, `0 8px 24px rgba(0,0,0,0.06)` for floating ones. + +## Motion + +One easing curve for everything, `cubic-bezier(0.22, 1, 0.36, 1)`. + +| Use | Duration | +| --- | --- | +| Hover, press, toggles | 120 to 180ms | +| Menus, popovers, tooltips | 160 to 220ms | +| Entrances, sheets, layout changes | 300 to 500ms | +| Stagger between items | 40 to 60ms, at most six items | + +- Rise distance on enter: 6 to 12px. Press scale: 0.97 to 0.98. +- Transition color, background, border, opacity and transform only. + +--- + +Part of unslop by Damir Sokolovsky ([@heydamir](https://x.com/heydamir)), CC BY 4.0. Keep this credit in copies and adaptations. diff --git a/vendor/interface-design/heydamir-unslop/skills/unslop/references/visuals.md b/vendor/interface-design/heydamir-unslop/skills/unslop/references/visuals.md new file mode 100644 index 0000000..d7e89c1 --- /dev/null +++ b/vendor/interface-design/heydamir-unslop/skills/unslop/references/visuals.md @@ -0,0 +1,84 @@ +# Visuals + +How to replace decorative filler with a visual that belongs to this product, and how to generate one when an image tool is available. + +## Decide what the visual is + +1. **Does the screen need one?** Dashboards, settings and most app screens do not. Landing heroes and empty states usually do. +2. **Is there an image tool?** For a landing hero, generate a scene from the world as the background of the whole hero, with the subject on one side and calm space for the headline on the other. Then show the product rising from the bottom edge. This is the strongest hero. +3. **Show the product.** Below the scene, or as the visual when there is no tool, build a preview in code of what the page already promises. Pick the one moment that proves the headline, not the whole app. A scheduling tool shows one week with a booked slot, a code review tool shows one diff with a comment, a habit app shows a streak on one card. Use the project's own components when they exist, otherwise careful HTML in the new palette, with plausible data and real looking names. Frame it like a window or a device only if that helps it read. +4. **Write the scene as one sentence.** Use the world and character from the direction and write the subject as one plain sentence before generating anything. If you cannot say it in one sentence, it is not clear yet. + +These examples only show the shape of a good sentence. Do not reuse them, derive yours from the product in front of you. + +- A tool that protects backups → a small lighthouse on a dark rocky coast with its beam reaching into fog +- A meditation app → a single cup of tea steaming on a windowsill at first light +- A logistics API → one container ship seen from straight above on a calm grey sea +- A language learning app → a stack of handwritten flashcards on a café table in afternoon sun + +Avoid the AI defaults, unless the product is literally about them. No glossy blobs, orbs, nebulas, chrome liquid, floating cubes, neon circuits, holograms, glowing brains, robots, rockets or crystal shapes. + +## Styles that work + +Pick one that fits the character, and keep it for the whole page. + +- **Painted background art.** A calm sky or landscape with one subject, painted like the backgrounds of modern Japanese animated films, crisp clouds and soft light, with open sky left for the text. Good for warm, hopeful, human products. +- **Cinematic photo of one object.** A single real thing in simple dramatic light on a plain field, like a lighthouse in fog or a paper object in mid air. Good for calm, serious, trustworthy products. +- **Soft stylized 3D.** One small scene or object with matte materials and gentle light, like a doorway standing in a meadow. Good for playful or friendly products. +- **Product object render.** A real looking object from the product, like a payment card, a phone or a package, studio lit with true materials, used as a moment inside the interface. Good for anything that touches the physical world. +- **Light and optics.** One physical phenomenon on a dark field, like light splitting through a prism. Good for technical or data products. +- **Engraving or print.** A monochrome engraving or woodcut in one ink on a soft tinted page, with type and palette that belong to the same world. Good for products with heritage, craft or an editorial voice. +- **A code treatment over an image.** Rendering a generated image as glyphs, dots or a duotone in the page palette, so it feels made for the page. See [effects.md](effects.md). + +What tends to fail: + +- Prompts that ask for film grain, paper grain or a mottled texture. Models bake a fine checker pattern into the image. Add texture in code if you need it. +- A print style, like engraving, woodcut or risograph, on one image while the rest of the page is a plain modern interface. It reads as a filter. The same styles work well when the whole brand lives in them, with matching type and a paper palette, like a monochrome engraving on a soft tinted page. +- Several subjects or a busy collage. The idea stops reading in a second. +- Purple and blue glossy abstractions of any kind. + +## Objects for moments + +Besides whole scenes, generate single objects to place into the interface, like a payment card, a phone, a parcel, a coffee cup or a notebook from the product's world. They make a coded preview feel real. + +- Ask for one object, studio lit, on a plain background in the exact page color, with soft contact shadow and nothing else in frame. +- Ask for the angle you need in the layout, like "tilted 20 degrees, seen slightly from above". +- If the tool can remove backgrounds, cut the object out. Otherwise blend it with a mask into the page color. +- Place it overlapping the edge of a card or window, a little rotated, with a soft shadow in the ramp's darkest color, so it sits in the same space as the UI. +- Background removal deletes dark areas that are close to the background color, like a switched off black screen. Ask for the screen in a contrasting flat color, or rebuild the alpha from the outer edge of the cutout. +- To show real UI on a generated device, generate it straight on with no perspective, measure the screen rectangle, lay HTML over it and rotate both together. The text stays sharp and real. +- Everything inside one visual tells the same story. If the list shows invoice INV-0142 for a client, the phone shows the same client, number and amount. + +## Generate it + +Look for an image generation tool among the tools you have, for example an MCP server for fal, Replicate, OpenAI images, Higgsfield or a design tool. If there is one: + +- Show the user the prompt, the model and the size, and ask before running, since generation can cost money. +- Run once. Judge the result against the one sentence and the palette before trying again, and change the prompt rather than rerolling the same one. +- Save the result into the project's assets, not a temporary URL. + +If there is no tool and the screen needs a scene or an object, say so before the Visuals pass and offer to connect one, like an MCP server for fal, Replicate, OpenAI images or Higgsfield, then go on once it is connected. If the user skips it, leave the slot empty and put the prompt in your report, not on the page. The empty slot is a plain surface step of the exact size with the radius of the layout and no text inside, so nothing unfinished can ship by accident. Tell the user which kind of service can generate it and the file name to save it under. + +## Prompt recipe + +Build the prompt from these parts, in this order: + +- **Subject.** The one sentence, concrete and physical. +- **Setting.** Where it is, kept simple. +- **Light and palette.** Matched to the page. Name the page background color so the edges can blend into it, and the direction of the light. +- **Style.** One clear direction that fits the character, like cinematic photo, soft 3D render, editorial illustration, risograph print or painted background. Pick what fits the product, not what is trending. +- **Composition.** Where the subject sits so the text has room, like "subject on the right half, calm empty space on the left". For a scene behind the whole hero, ask for 16:9 or wider, keep the lower fifth calm too, since the product view rises over it, and fade the edges into the page color in code. +- **Exclusions.** No text, no letters, no logos, no UI, no watermark. +- **Format.** The aspect ratio of the slot at twice the display size. + +## Put it on the page + +- Export as WebP or AVIF at twice the display size, with a real `alt` that says what is shown. +- Match the image edges to the page background. If they do not match, fade them with a mask, like `mask-image: radial-gradient(closest-side, #000 70%, transparent)`, or a gradient overlay in the page color. A hard rectangle on a page of a different color is a sign of slop. +- Keep it static or give it one slow, subtle motion that fits the subject. No parallax. +- A treatment in code, like a duotone into the page palette, can make a generated image feel native. Use one only when it fits the character, and never as new decoration. +- Load the hero image with high priority and give it fixed dimensions so the layout does not jump. + +--- + +Part of unslop by Damir Sokolovsky ([@heydamir](https://x.com/heydamir)), CC BY 4.0. Keep this credit in copies and adaptations. diff --git a/vendor/interface-design/heydamir-unslop/skills/unslop/scripts/snapshot.mjs b/vendor/interface-design/heydamir-unslop/skills/unslop/scripts/snapshot.mjs new file mode 100644 index 0000000..f4af5ce --- /dev/null +++ b/vendor/interface-design/heydamir-unslop/skills/unslop/scripts/snapshot.mjs @@ -0,0 +1,144 @@ +import { createRequire } from "node:module"; +import { existsSync, mkdirSync, writeFileSync } from "node:fs"; +import { dirname, join, resolve } from "node:path"; + +const usage = "usage: node snapshot.mjs [pass label] [--dark | --light] [--width=1440] [--height=810]"; +const args = process.argv.slice(2); +const flag = (name) => args.find((arg) => arg.startsWith(`--${name}=`))?.split("=")[1]; +const [url, out, label] = args.filter((arg) => !arg.startsWith("--")); +const width = flag("width") ?? "1440"; +const height = flag("height") ?? "810"; +const scheme = args.includes("--dark") ? "dark" : args.includes("--light") ? "light" : "no-preference"; +if (!url || !out) { + console.error(usage); + process.exit(1); +} + +async function browserKit() { + for (const name of ["playwright", "playwright-core"]) { + try { + return await import(name); + } catch {} + try { + return createRequire(join(process.cwd(), "package.json"))(name); + } catch {} + } + console.error("Playwright is not installed. In your project, run `npm install -D playwright` followed by `npx playwright install chromium`."); + process.exit(1); +} + +const chrome = [process.env.CHROME_PATH, "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", "/usr/bin/google-chrome", "/usr/bin/chromium"].find((path) => path && existsSync(path)); +const { chromium } = await browserKit(); +const browser = await chromium.launch(chrome ? { executablePath: chrome } : {}); +try { + const page = await browser.newPage({ viewport: { width: Number(width), height: Number(height) }, colorScheme: scheme }); + await page.goto(url, { waitUntil: "networkidle" }); + await page.evaluate(() => document.fonts.ready); + await page.waitForTimeout(600); + const html = await page.evaluate(async ([label, scheme]) => { + const LIMIT = 3_000_000; + const absolute = (value, base = location.href) => { + try { + return new URL(value, base).href; + } catch { + return value; + } + }; + const inlined = new Map(); + const inline = async (href) => { + if (!href || href.startsWith("data:")) return href; + if (!inlined.has(href)) { + inlined.set( + href, + fetch(href) + .then((response) => (response.ok ? response.blob() : null)) + .then((blob) => + blob && blob.size < LIMIT + ? new Promise((done) => { + const reader = new FileReader(); + reader.onload = () => done(reader.result); + reader.readAsDataURL(blob); + }) + : href, + ) + .catch(() => href), + ); + } + return inlined.get(href); + }; + const urls = async (css, base) => { + const found = [...css.matchAll(/url\((['"]?)(?!data:)([^)'"]+)\1\)/g)]; + let result = css; + for (const [whole, , value] of found) { + if (/^(#|%23)/.test(value.trim())) continue; + const data = await inline(absolute(value.trim(), base)); + result = result.split(whole).join(`url("${data}")`); + } + return result; + }; + + const css = []; + for (const sheet of document.styleSheets) { + const owner = sheet.ownerNode; + let text = ""; + let base = location.href; + if (sheet.href) { + base = sheet.href; + text = await fetch(sheet.href).then((response) => (response.ok ? response.text() : "")).catch(() => ""); + } else if (owner && owner.textContent.trim()) { + text = owner.textContent; + } + if (!text) { + try { + text = [...sheet.cssRules].map((rule) => rule.cssText).join("\n"); + } catch {} + } + const media = sheet.media && sheet.media.mediaText; + css.push(media && media !== "all" ? `@media ${media} {\n${text}\n}` : text); + css[css.length - 1] = await urls(css[css.length - 1], base); + } + + const copy = document.documentElement.cloneNode(true); + copy.querySelectorAll("script, noscript, link[rel='stylesheet'], link[rel='preload'], link[rel='modulepreload'], style").forEach((node) => node.remove()); + copy.querySelectorAll("[srcset]").forEach((node) => node.removeAttribute("srcset")); + copy.querySelectorAll("source").forEach((node) => node.remove()); + copy.querySelectorAll("[loading]").forEach((node) => node.removeAttribute("loading")); + for (const node of copy.querySelectorAll("img[src], video[poster], image")) { + const attr = node.tagName === "VIDEO" ? "poster" : node.tagName.toLowerCase() === "image" ? "href" : "src"; + const value = node.getAttribute(attr); + if (value) node.setAttribute(attr, await inline(absolute(value))); + } + copy.querySelectorAll("a[href]").forEach((node) => node.setAttribute("href", absolute(node.getAttribute("href")))); + for (const node of copy.querySelectorAll("[style*='url(']")) { + node.setAttribute("style", await urls(node.getAttribute("style"), location.href)); + } + const style = document.createElement("style"); + let text = css.join("\n"); + if (scheme === "dark" || scheme === "light") { + const other = scheme === "dark" ? "light" : "dark"; + text = text + .replace(new RegExp(`prefers-color-scheme\\s*:\\s*${scheme}`, "g"), "min-width: 0px") + .replace(new RegExp(`prefers-color-scheme\\s*:\\s*${other}`, "g"), "max-width: 0px"); + const meta = document.createElement("meta"); + meta.name = "color-scheme"; + meta.content = scheme; + copy.querySelector("head").prepend(meta); + } + style.textContent = text; + copy.querySelector("head").appendChild(style); + if (label) { + const meta = document.createElement("meta"); + meta.name = "unslop-pass"; + meta.content = label; + copy.querySelector("head").prepend(meta); + } + return "\n" + copy.outerHTML; + }, [label ?? "", scheme]); + mkdirSync(dirname(resolve(out)), { recursive: true }); + writeFileSync(resolve(out), html); + await page.evaluate(() => window.scrollTo(0, 0)); + await page.screenshot({ path: resolve(out).replace(/\.html?$/, "") + ".live.png" }); + console.log(`saved ${resolve(out)}${label ? ` as ${label}` : ""}`); +} finally { + await browser.close(); +} diff --git a/vendor/interface-design/heydamir-unslop/skills/unslop/scripts/story.mjs b/vendor/interface-design/heydamir-unslop/skills/unslop/scripts/story.mjs new file mode 100644 index 0000000..dd6e826 --- /dev/null +++ b/vendor/interface-design/heydamir-unslop/skills/unslop/scripts/story.mjs @@ -0,0 +1,461 @@ +import { createRequire } from "node:module"; +import { createServer } from "node:http"; +import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { execFileSync } from "node:child_process"; +import { dirname, extname, join, normalize, resolve } from "node:path"; + +const usage = "usage: node story.mjs [--wide], or node story.mjs --play to watch it live in a browser window"; +const args = process.argv.slice(2); +const [input, out] = args.filter((arg) => !arg.startsWith("--")); +const wide = args.includes("--wide"); +const play = args.includes("--play"); +if (!input || (!out && !play)) { + console.error(usage); + process.exit(1); +} + +async function browserKit() { + for (const name of ["playwright", "playwright-core"]) { + try { + return await import(name); + } catch {} + try { + return createRequire(join(process.cwd(), "package.json"))(name); + } catch {} + } + console.error("Playwright is not installed. In your project, run `npm install -D playwright` followed by `npx playwright install chromium`."); + process.exit(1); +} + +try { + execFileSync("ffmpeg", ["-version"], { stdio: "ignore" }); +} catch { + console.error("ffmpeg is not installed. Install it to render the video, for example `brew install ffmpeg`."); + process.exit(1); +} + +const file = resolve(input); +const root = dirname(file); +const html = readFileSync(file, "utf8"); +const meta = (name) => new RegExp(` step.trim()) + .filter(Boolean); +if (steps.length === 0) { + console.error('story.html needs listing the steps in order'); + process.exit(1); +} +const [frameX = 0, frameY = 0, frameWidth = 960] = (meta("unslop-frame") ?? "0 0 960").split(/\s+/).map(Number); +const kindOf = (text) => (/spac/i.test(text) ? "spacing" : /detail|radius|corner/i.test(text) ? "details" : null); + +const FRAME = wide ? { width: 1920, height: 1080 } : { width: 1080, height: 1080 }; +const PAGE_WIDTH = 1440; +const MARGIN = 72; +const VIEW = 0.75; +const scale = (FRAME.width - MARGIN) / frameWidth; +const offsetX = MARGIN / scale - frameX; +const offsetY = MARGIN / scale - frameY; +const padLeft = Math.max(0, offsetX); +const padTop = Math.max(0, offsetY); +const left = Math.min(0, offsetX) * scale; +const top = Math.min(0, offsetY) * scale; +const pageWidth = Math.max(PAGE_WIDTH + padLeft, Math.ceil((FRAME.width - left) / scale) + 2); +const padRight = pageWidth - PAGE_WIDTH - padLeft; +const pageHeight = Math.max(900 + padTop, Math.ceil((FRAME.height - top) / scale) + 2); + +const stage = ` + +
+
unslop by @heydamir
+ +`; + +const runtime = ` +(() => { + const red = "#ff4d6a"; + const spring = "cubic-bezier(0.3, 1.12, 0.45, 1)"; + const ease = "cubic-bezier(0.45, 0, 0.2, 1)"; + const layout = ["padding", "margin", "gap", "row-gap", "column-gap", "width", "height", "min-width", "min-height", "max-width", "max-height", "top", "right", "bottom", "left", "inset", "font-size", "line-height", "letter-spacing", "font-weight", "transform", "translate", "scale", "rotate", "border-width", "outline-width", "flex-basis", "grid-template-columns", "grid-template-rows"]; + const tint = ["color", "background-color", "border-color", "outline-color", "fill", "stroke", "stroke-dashoffset", "stroke-width", "box-shadow", "text-shadow", "border-radius", "opacity", "filter", "backdrop-filter", "clip-path", "text-decoration-color", "-webkit-text-fill-color"]; + const transition = [...layout.map((p) => p + " 700ms " + spring + " var(--wave, 0ms)"), ...tint.map((p) => p + " 900ms " + ease + " var(--wave, 0ms)")].join(", "); + const rise = "cubic-bezier(0.17, 0.84, 0.44, 1)"; + const steps = ${JSON.stringify(steps)}; + let css = \` + :root { interpolate-size: allow-keywords; } + html.story-live *, html.story-live *::before, html.story-live *::after { transition: \${transition} !important; } + .swap { display: inline-grid; justify-items: start; align-items: start; vertical-align: top; overflow: visible; } + div.swap, p.swap, h1.swap, h2.swap, h3.swap, li.swap { display: grid; } + .swap > .from, .swap > .to { grid-area: 1 / 1; } + .swap > .to { opacity: 0; pointer-events: none; } + .swap > .to .ch { display: inline-block; opacity: 0; transform: translateY(40%); filter: blur(8px); } + html.story-live .swap > .to .ch { transition: opacity 700ms \${rise} calc(var(--wave, 0ms) + 140ms + var(--ci) * 16ms), transform 700ms \${rise} calc(var(--wave, 0ms) + 140ms + var(--ci) * 16ms), filter 700ms \${rise} calc(var(--wave, 0ms) + 140ms + var(--ci) * 16ms) !important; } + [data-in] { opacity: 0; transform: translateY(10px) scale(0.98); } + \`; + steps.forEach((_, index) => { + const k = index + 1; + css += \` + html.s\${k} .swap[data-at="\${k}"] > .from { opacity: 0; filter: blur(10px); transform: translateY(-14px); } + html.s\${k} .swap[data-at="\${k}"] > .to { opacity: 1; } + html.s\${k} .swap[data-at="\${k}"] > .to .ch { opacity: 1; transform: none; filter: none; } + html.s\${k} [data-in="\${k}"] { opacity: 1; transform: none; } + html.s\${k} [data-out="\${k}"] { opacity: 0; max-height: 0 !important; max-width: 0 !important; margin-top: 0 !important; margin-bottom: 0 !important; padding: 0 !important; border-width: 0 !important; } + \`; + }); + + function letters(node) { + let index = 0; + const walk = (el) => { + [...el.childNodes].forEach((child) => { + if (child.nodeType === 3) { + const frag = document.createDocumentFragment(); + child.textContent.split(/(\\s+)/).forEach((part) => { + if (!part) return; + if (/^\\s+$/.test(part)) return frag.appendChild(document.createTextNode(part)); + const word = document.createElement("span"); + word.style.display = "inline-block"; + word.style.whiteSpace = "nowrap"; + [...part].forEach((char) => { + const span = document.createElement("span"); + span.className = "ch"; + span.style.setProperty("--ci", index++); + span.textContent = char; + word.appendChild(span); + }); + frag.appendChild(word); + }); + child.replaceWith(frag); + } else if (child.nodeType === 1 && child.tagName.toLowerCase() !== "svg") { + walk(child); + } + }); + }; + walk(node); + } + + const conf = window.parent.__stage.conf; + function layer() { + return window.parent.document.getElementById("story-layer"); + } + + const box = (el) => el.getBoundingClientRect(); + const sx = (x) => conf.x + x * conf.scale; + const sy = (y) => conf.y + y * conf.scale; + function put(className, style, html) { + const node = window.parent.document.createElement("div"); + node.className = className; + Object.assign(node.style, style); + if (html) node.innerHTML = html; + layer().appendChild(node); + return node; + } + const rect = (left, top, width, height) => ({ left: sx(left) + "px", top: sy(top) + "px", width: width * conf.scale + "px", height: height * conf.scale + "px" }); + + function audit() { + document.querySelectorAll("[data-slop]").forEach((el, index) => { + const b = box(el); + if (b.width < 4 || b.bottom < 0 || b.top > innerHeight) return; + const pad = 8 / conf.scale; + const wrap = put("s-fade", { ...rect(b.left - pad, b.top - pad, b.width + pad * 2, b.height + pad * 2), transitionDelay: index * 70 + "ms" }); + wrap.innerHTML = '
' + + [["left", "top"], ["right", "top"], ["left", "bottom"], ["right", "bottom"]].map(([x, y]) => '').join("") + + '' + el.dataset.slop + ""; + }); + } + + function spacing() { + const groups = new Map(); + document.querySelectorAll("[data-gap]").forEach((el) => { + const key = el.dataset.gap || "main"; + if (!groups.has(key)) groups.set(key, []); + groups.get(key).push(el); + }); + const stageWidth = window.parent.innerWidth; + const several = groups.size > 1; + const pairs = []; + groups.forEach((list) => { + const items = list.filter((el) => box(el).height > 0 && box(el).top < innerHeight).sort((a, b) => box(a).top - box(b).top); + for (let i = 1; i < items.length; i++) { + if (box(items[i]).top - box(items[i - 1]).bottom <= 2) continue; + pairs.push({ group: items, above: items[i - 1], below: items[i], nodes: { + fill: put("s-fill s-x", {}), top: put("s-hair s-x", {}), bottom: put("s-hair s-x", {}), + ruler: put("s-ink s-y", {}), capTop: put("s-ink s-x", {}), capBottom: put("s-ink s-x", {}), + tag: put("s-tag s-fade", {}), + } }); + } + }); + return () => { + for (const { group, above, below, nodes } of pairs) { + const a = box(above); + const b = box(below); + const left = sx(Math.min(...group.map((el) => box(el).left))); + const ruler = Math.max(14, left - 20); + const top = sy(a.bottom); + const size = Math.max(0, (b.top - a.bottom) * conf.scale); + const value = Math.round(b.top - a.bottom); + const right = several ? sx(Math.max(...group.map((el) => box(el).right))) + 24 : stageWidth; + const start = several ? ruler - 8 : 0; + const band = { left: start + "px", width: right - start + "px", transformOrigin: ruler - start + "px 50%" }; + Object.assign(nodes.fill.style, band, { top: top + "px", height: size + "px" }); + Object.assign(nodes.top.style, band, { top: top + "px", height: "1px" }); + Object.assign(nodes.bottom.style, band, { top: top + size - 1 + "px", height: "1px" }); + Object.assign(nodes.ruler.style, { left: ruler + "px", top: top + "px", width: "1.5px", height: size + "px", transformOrigin: "50% 0" }); + Object.assign(nodes.capTop.style, { left: ruler - 4 + "px", top: top + "px", width: "9px", height: "1.5px" }); + Object.assign(nodes.capBottom.style, { left: ruler - 4 + "px", top: top + size - 1.5 + "px", width: "9px", height: "1.5px" }); + const room = ruler - 10 > 64; + Object.assign(nodes.tag.style, { left: (room ? ruler - 10 : ruler + 12) + "px", top: top + size / 2 - 15 + "px", translate: room ? "-100% 0" : "0 0" }); + const text = value + "px"; + if (nodes.tag.textContent !== text) nodes.tag.textContent = text; + const hidden = value < 4 ? "0" : ""; + Object.values(nodes).forEach((node) => { + if (node.style.visibility !== (hidden ? "hidden" : "")) node.style.visibility = hidden ? "hidden" : ""; + }); + } + }; + } + + function details() { + const items = [...document.querySelectorAll("[data-radius]")].filter((el) => box(el).bottom > 0 && box(el).top < innerHeight).map((el) => { + const wrap = put("s-fade", {}, '
'); + return { el, wrap, outline: wrap.firstChild, tag: put("s-tag s-fade", {}) }; + }); + return () => { + for (const { el, wrap, outline, tag } of items) { + const b = box(el); + const radius = Math.round(Math.min(parseFloat(getComputedStyle(el).borderTopLeftRadius) || 0, b.width / 2, b.height / 2)); + const pad = 5 / conf.scale; + Object.assign(wrap.style, rect(b.left - pad, b.top - pad, b.width + pad * 2, b.height + pad * 2)); + outline.style.borderRadius = radius * conf.scale + 5 + "px"; + Object.assign(tag.style, { left: sx(b.left) + "px", top: sy(b.bottom) + 12 + "px" }); + const text = "r " + radius; + if (tag.textContent !== text) tag.textContent = text; + } + }; + } + + const kinds = { audit, spacing, details }; + const managed = new WeakMap(); + let current = null; + let stepping = false; + + window.__story = { + install() { + const style = document.createElement("style"); + style.id = "story-style"; + style.textContent = css; + document.head.appendChild(style); + document.querySelectorAll(".swap > .to").forEach(letters); + document.querySelectorAll("[data-out]").forEach((el) => { + const b = box(el); + if (getComputedStyle(el).display === "inline") el.style.display = "inline-block"; + el.style.overflow = "hidden"; + el.style.maxHeight = Math.ceil(b.height) + "px"; + el.style.maxWidth = Math.ceil(b.width) + "px"; + }); + document.querySelectorAll("body *").forEach((el) => { + const b = box(el); + const wave = Math.round(Math.min(420, Math.max(0, b.top) / innerHeight * 320 + Math.max(0, b.left) / innerWidth * 100)); + el.style.setProperty("--wave", wave + "ms"); + }); + document.querySelectorAll("*").forEach((el) => { + if (getComputedStyle(el).animationName !== "none") el.style.animationPlayState = "paused"; + }); + document.documentElement.offsetHeight; + document.documentElement.classList.add("story-live"); + const follow = () => { + if (current && !stepping) current(); + requestAnimationFrame(follow); + }; + requestAnimationFrame(follow); + }, + reset() { + const root = document.documentElement; + root.classList.remove("story-live", ...steps.map((_, index) => "s" + (index + 1))); + layer().className = ""; + layer().innerHTML = ""; + current = null; + root.offsetHeight; + root.classList.add("story-live"); + }, + tick(ms) { + stepping = true; + for (const animation of [...document.getAnimations(), ...window.parent.document.getAnimations()]) { + if (!managed.has(animation)) { + if (animation.playState !== "running") continue; + managed.set(animation, ms); + animation.pause(); + } + const time = ms - managed.get(animation); + const end = animation.effect ? animation.effect.getComputedTiming().endTime : Infinity; + if (Number.isFinite(end) && time >= end) animation.finish(); + else animation.currentTime = time; + } + if (current) current(); + }, + step(k) { + document.documentElement.classList.add("s" + k); + }, + show(kind) { + const node = layer(); + node.className = ""; + node.innerHTML = ""; + const update = (kinds[kind] || (() => {}))(); + current = typeof update === "function" ? update : null; + if (current) current(); + node.offsetHeight; + node.classList.add("s-on"); + }, + settle() { + layer().classList.add("s-on", "s-fixed"); + }, + clear() { + layer().classList.remove("s-on"); + }, + }; +})(); +`; + +const types = { ".html": "text/html", ".css": "text/css", ".js": "text/javascript", ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".webp": "image/webp", ".avif": "image/avif", ".svg": "image/svg+xml", ".woff2": "font/woff2", ".woff": "font/woff", ".mp4": "video/mp4" }; +const server = createServer((request, response) => { + const path = decodeURIComponent(new URL(request.url, "http://local").pathname); + if (path === "/__stage.html") { + response.writeHead(200, { "content-type": "text/html" }); + return response.end(stage); + } + const target = normalize(join(root, path.replace(/^\/__story\//, "/"))); + if (!target.startsWith(root) || !existsSync(target)) { + response.writeHead(404); + return response.end(); + } + response.writeHead(200, { "content-type": types[extname(target).toLowerCase()] ?? "application/octet-stream" }); + response.end(readFileSync(target)); +}); +await new Promise((done) => server.listen(0, "127.0.0.1", done)); +const base = `http://127.0.0.1:${server.address().port}`; + +const FPS = 60; +const LENGTH = 10; +const frames = join(root, ".story-frames"); +if (!play) { + rmSync(frames, { recursive: true, force: true }); + mkdirSync(frames, { recursive: true }); +} + +const chrome = [process.env.CHROME_PATH, "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", "/usr/bin/google-chrome", "/usr/bin/chromium"].find((path) => path && existsSync(path)); +const { chromium } = await browserKit(); +const browser = await chromium.launch({ ...(chrome ? { executablePath: chrome } : {}), headless: !play }); +const sleep = (ms) => new Promise((done) => setTimeout(done, Math.max(0, ms))); +let total = 0; +try { + const page = await browser.newPage(play ? { viewport: { width: Math.round(FRAME.width * VIEW), height: Math.round(FRAME.height * VIEW) } } : { viewport: FRAME, deviceScaleFactor: 2 }); + await page.goto(`${base}/__stage.html`, { waitUntil: "networkidle" }); + const frame = page.frames().find((item) => item !== page.mainFrame()); + await frame.waitForLoadState("networkidle"); + await frame.evaluate(async () => { + const families = new Set(); + for (const sheet of document.styleSheets) { + let rules = []; + try { + rules = [...sheet.cssRules]; + } catch {} + const walk = (list) => list.forEach((rule) => { + if (rule.cssRules) walk([...rule.cssRules]); + const value = rule.style && rule.style.fontFamily; + if (value) value.split(",").forEach((name) => families.add(name.trim().replace(/^["']|["']$/g, ""))); + }); + walk(rules); + } + await Promise.all([...families].flatMap((family) => [400, 500, 600, 700, 800].map((weight) => document.fonts.load(`${weight} 16px "${family}"`).catch(() => null)))); + await document.fonts.ready; + }); + await frame.addStyleTag({ content: `html { padding: ${padTop}px ${padRight}px 0 ${padLeft}px !important; }` }); + await frame.addScriptTag({ content: runtime }); + await frame.evaluate(() => window.__story.install()); + await page.waitForTimeout(300); + + const slop = await frame.evaluate(() => document.querySelectorAll("[data-slop]").length); + const call = (name, value) => () => frame.evaluate(([name, value]) => window.__story[name](value), [name, value]); + const plan = []; + const at = (time, run) => plan.push({ time, run }); + let cursor = 0.5; + if (slop) { + at(cursor, call("show", "audit")); + at(cursor + 0.9, call("clear")); + cursor += 1.1; + } + const blocks = steps.map((name) => (kindOf(name) ? 1.75 : 1.25)); + const fit = Math.min(1, (LENGTH - cursor - 1.6) / blocks.reduce((sum, block) => sum + block, 0)); + steps.forEach((name, index) => { + const kind = kindOf(name); + if (kind) { + at(cursor, call("show", kind)); + at(cursor + 0.35 * fit, call("step", index + 1)); + at(cursor + 1.35 * fit, call("settle")); + at(cursor + 1.7 * fit, call("clear")); + } else { + at(cursor, call("step", index + 1)); + } + cursor += blocks[index] * fit; + }); + total = cursor + 1.6; + plan.sort((a, b) => a.time - b.time); + + if (play) { + console.log("playing in the browser window, close it to stop"); + while (browser.isConnected()) { + await frame.evaluate(() => window.__story.reset()); + await sleep(400); + const begin = Date.now(); + for (const { time, run } of plan) { + await sleep(begin + time * 1000 - Date.now()); + await run(); + } + await sleep(begin + total * 1000 - Date.now()); + } + } else { + let next = 0; + for (let index = 0; index <= Math.round(total * FPS); index++) { + const now = index / FPS; + while (next < plan.length && plan[next].time <= now + 1e-6) await plan[next++].run(); + await frame.evaluate((ms) => window.__story.tick(ms), now * 1000); + await page.screenshot({ path: join(frames, `f-${String(index).padStart(5, "0")}.jpg`), type: "jpeg", quality: 92 }); + } + } +} catch (error) { + if (!play) throw error; +} finally { + await browser.close().catch(() => {}); + server.close(); +} +if (play) process.exit(0); + +execFileSync("ffmpeg", ["-y", "-loglevel", "error", "-framerate", String(FPS), "-i", join(frames, "f-%05d.jpg"), "-vf", `scale=${FRAME.width * 2}:${FRAME.height * 2}:flags=lanczos`, "-c:v", "libx264", "-pix_fmt", "yuv420p", "-crf", "18", "-preset", "slow", "-movflags", "+faststart", resolve(out)]); +rmSync(frames, { recursive: true, force: true }); +console.log(`saved ${resolve(out)}, ${steps.length} steps, ${total.toFixed(1)}s`); diff --git a/vendor/interface-design/impeccable/agents/impeccable-documenter.md b/vendor/interface-design/impeccable/agents/impeccable-documenter.md index eb12233..5884a78 100644 --- a/vendor/interface-design/impeccable/agents/impeccable-documenter.md +++ b/vendor/interface-design/impeccable/agents/impeccable-documenter.md @@ -14,16 +14,16 @@ Complete the check within your turn ceiling. Batch Reads, take `reference/docume ## Input Contract -Expect: the project root; the artifact path(s); the direction contract text (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; the path to the skill's `reference/document.md`; and the boundary to write at (project or app root). An existing DESIGN.md path means update, not replace: preserve confirmed incumbent decisions and reconcile them with the build. +Expect: the project root; the artifact path(s); the direction contract text (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; the path to the skill's `reference/document.md`; the boundary to write at (project or app root); and, when a DESIGN.md already exists there, the user's choice for it (refresh, overwrite, or merge), the answer `reference/document.md` requires before any rewrite, which the parent collects. A merge preserves confirmed incumbent decisions and reconciles them with the build. ## Workflow 1. Read `reference/document.md` in full; it is the operating spec for DESIGN.md's format, token schema, sidecar, and section order. Follow it exactly. 2. Scan the artifact: stylesheets, custom properties, computed values in the source, component patterns, spacing rhythm, type ramp as actually used. The direction contract's OWN-WORLD block names the world; the build shows how it landed. Where they diverge, the build wins and the prose may note the divergence. -3. For a new world or approved system change, write DESIGN.md and its sidecar from durable, reused rules in the build. Ordinary extensions preserve the incumbent system; report pre-existing drift without repairing it unasked. Do not write merely to prove this pass ran. +3. For a new world or approved system change, write DESIGN.md and its sidecar from durable, reused rules in the build, honoring the user's choice when a DESIGN.md already exists; when it exists and no choice arrived, write nothing and return that the choice is owed. Ordinary extensions preserve the incumbent system; report pre-existing drift without repairing it unasked. Do not write merely to prove this pass ran. 4. Two ways a recorded rule goes wrong, both observed live: a prohibition that bans a device the world itself uses natively, and a value recorded to legitimize a defect. Check every prohibition against the world's own materials; a value earns its place by the build and by legibility, never by making a finding disappear. 5. Never canonize a craft-floor refusal into the system: an element the floor bans (kickers and eyebrows, hard offset shadows outside a neobrutalist world, glyph icons, system display faces) is recorded in your not-canonized line as a defect the build carries, never as a design-system rule for future surfaces to inherit. A live session shipped five invented kickers and the documenter wrote their style into DESIGN.md; that is how one violation becomes the house style. ## Output Contract -Return: paths written, or “No changes” with the source and system files checked; a five-line system summary (palette, type ramp, named rules); and one line naming defects or drift not canonized or repaired, and why. No other prose. +Return: paths written, or “No changes” with the source and system files checked and, when an existing DESIGN.md stopped the write, the choice owed; a five-line system summary (palette, type ramp, named rules); and one line naming defects or drift not canonized or repaired, and why. No other prose. diff --git a/vendor/interface-design/impeccable/hooks/hooks.json b/vendor/interface-design/impeccable/hooks/hooks.json index 7d670ea..06b857e 100644 --- a/vendor/interface-design/impeccable/hooks/hooks.json +++ b/vendor/interface-design/impeccable/hooks/hooks.json @@ -5,7 +5,11 @@ "hooks": [ { "type": "command", - "command": "[ ! -f \"${CLAUDE_PLUGIN_ROOT}/skills/impeccable/scripts/impeccable\" ] || \"${CLAUDE_PLUGIN_ROOT}/skills/impeccable/scripts/impeccable\" hook", + "command": "[ ! -f \"${CLAUDE_PLUGIN_ROOT}/skills/impeccable/scripts/impeccable\" ] || sh \"${CLAUDE_PLUGIN_ROOT}/skills/impeccable/scripts/impeccable\" hook", + "windows": "cmd /c if exist ${CLAUDE_PLUGIN_ROOT}\\skills\\impeccable\\scripts\\impeccable.cmd ${CLAUDE_PLUGIN_ROOT}\\skills\\impeccable\\scripts\\impeccable.cmd hook", + "env": { + "IMPECCABLE_HOOK_HARNESS": "vscode" + }, "timeout": 5, "statusMessage": "Preparing build session" } @@ -18,7 +22,11 @@ "hooks": [ { "type": "command", - "command": "[ ! -f \"${CLAUDE_PLUGIN_ROOT}/skills/impeccable/scripts/impeccable\" ] || \"${CLAUDE_PLUGIN_ROOT}/skills/impeccable/scripts/impeccable\" hook", + "command": "[ ! -f \"${CLAUDE_PLUGIN_ROOT}/skills/impeccable/scripts/impeccable\" ] || sh \"${CLAUDE_PLUGIN_ROOT}/skills/impeccable/scripts/impeccable\" hook", + "windows": "cmd /c if exist ${CLAUDE_PLUGIN_ROOT}\\skills\\impeccable\\scripts\\impeccable.cmd ${CLAUDE_PLUGIN_ROOT}\\skills\\impeccable\\scripts\\impeccable.cmd hook", + "env": { + "IMPECCABLE_HOOK_HARNESS": "vscode" + }, "timeout": 5, "statusMessage": "Checking UI changes" } @@ -30,7 +38,11 @@ "hooks": [ { "type": "command", - "command": "[ ! -f \"${CLAUDE_PLUGIN_ROOT}/skills/impeccable/scripts/impeccable\" ] || \"${CLAUDE_PLUGIN_ROOT}/skills/impeccable/scripts/impeccable\" hook", + "command": "[ ! -f \"${CLAUDE_PLUGIN_ROOT}/skills/impeccable/scripts/impeccable\" ] || sh \"${CLAUDE_PLUGIN_ROOT}/skills/impeccable/scripts/impeccable\" hook", + "windows": "cmd /c if exist ${CLAUDE_PLUGIN_ROOT}\\skills\\impeccable\\scripts\\impeccable.cmd ${CLAUDE_PLUGIN_ROOT}\\skills\\impeccable\\scripts\\impeccable.cmd hook", + "env": { + "IMPECCABLE_HOOK_HARNESS": "vscode" + }, "timeout": 30, "statusMessage": "Design deep pass" } diff --git a/vendor/interface-design/impeccable/skills/impeccable/reference/degraded/asset-producer.md b/vendor/interface-design/impeccable/skills/impeccable/reference/degraded/asset-producer.md index 216dc91..10e92bd 100644 --- a/vendor/interface-design/impeccable/skills/impeccable/reference/degraded/asset-producer.md +++ b/vendor/interface-design/impeccable/skills/impeccable/reference/degraded/asset-producer.md @@ -1,5 +1,5 @@ -This harness has no subagent capability, so you are running this role inline. Step fully out of the work you just finished, adopt only this file's instructions for the pass, and disclose the substitution in one line when you report. Where the text below addresses a parent agent, you are both parties: produce the full output contract first, then act on it yourself. +This harness has no subagent capability, so you are running this role inline. Step fully out of the work you just finished, adopt only this file's instructions for the pass, and disclose the substitution in one line of your closing message to the user, outside the output contract. Where the text below addresses a parent agent, you are both parties: produce the full output contract first, then act on it yourself. # Impeccable Asset Producer diff --git a/vendor/interface-design/impeccable/skills/impeccable/reference/degraded/documenter.md b/vendor/interface-design/impeccable/skills/impeccable/reference/degraded/documenter.md index e9dfb79..d1640cc 100644 --- a/vendor/interface-design/impeccable/skills/impeccable/reference/degraded/documenter.md +++ b/vendor/interface-design/impeccable/skills/impeccable/reference/degraded/documenter.md @@ -1,5 +1,5 @@ -This harness has no subagent capability, so you are running this role inline. Step fully out of the work you just finished, adopt only this file's instructions for the pass, and disclose the substitution in one line when you report. Where the text below addresses a parent agent, you are both parties: produce the full output contract first, then act on it yourself. +This harness has no subagent capability, so you are running this role inline. Step fully out of the work you just finished, adopt only this file's instructions for the pass, and disclose the substitution in one line of your closing message to the user, outside the output contract. Where the text below addresses a parent agent, you are both parties: produce the full output contract first, then act on it yourself. # Impeccable Documenter @@ -9,16 +9,16 @@ Complete the check within your turn ceiling. Batch Reads, take `reference/docume ## Input Contract -Expect: the project root; the artifact path(s); the direction contract text (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; the path to the skill's `reference/document.md`; and the boundary to write at (project or app root). An existing DESIGN.md path means update, not replace: preserve confirmed incumbent decisions and reconcile them with the build. +Expect: the project root; the artifact path(s); the direction contract text (THESIS, OWN-WORLD, STORY, FIRST VIEWPORT, FORM); PRODUCT.md path; the path to the skill's `reference/document.md`; the boundary to write at (project or app root); and, when a DESIGN.md already exists there, the user's choice for it (refresh, overwrite, or merge), the answer `reference/document.md` requires before any rewrite, which the parent collects. A merge preserves confirmed incumbent decisions and reconciles them with the build. ## Workflow 1. Read `reference/document.md` in full; it is the operating spec for DESIGN.md's format, token schema, sidecar, and section order. Follow it exactly. 2. Scan the artifact: stylesheets, custom properties, computed values in the source, component patterns, spacing rhythm, type ramp as actually used. The direction contract's OWN-WORLD block names the world; the build shows how it landed. Where they diverge, the build wins and the prose may note the divergence. -3. For a new world or approved system change, write DESIGN.md and its sidecar from durable, reused rules in the build. Ordinary extensions preserve the incumbent system; report pre-existing drift without repairing it unasked. Do not write merely to prove this pass ran. +3. For a new world or approved system change, write DESIGN.md and its sidecar from durable, reused rules in the build, honoring the user's choice when a DESIGN.md already exists; when it exists and no choice arrived, write nothing and return that the choice is owed. Ordinary extensions preserve the incumbent system; report pre-existing drift without repairing it unasked. Do not write merely to prove this pass ran. 4. Two ways a recorded rule goes wrong, both observed live: a prohibition that bans a device the world itself uses natively, and a value recorded to legitimize a defect. Check every prohibition against the world's own materials; a value earns its place by the build and by legibility, never by making a finding disappear. 5. Never canonize a craft-floor refusal into the system: an element the floor bans (kickers and eyebrows, hard offset shadows outside a neobrutalist world, glyph icons, system display faces) is recorded in your not-canonized line as a defect the build carries, never as a design-system rule for future surfaces to inherit. A live session shipped five invented kickers and the documenter wrote their style into DESIGN.md; that is how one violation becomes the house style. ## Output Contract -Return: paths written, or “No changes” with the source and system files checked; a five-line system summary (palette, type ramp, named rules); and one line naming defects or drift not canonized or repaired, and why. No other prose. \ No newline at end of file +Return: paths written, or “No changes” with the source and system files checked and, when an existing DESIGN.md stopped the write, the choice owed; a five-line system summary (palette, type ramp, named rules); and one line naming defects or drift not canonized or repaired, and why. No other prose. \ No newline at end of file diff --git a/vendor/interface-design/impeccable/skills/impeccable/reference/degraded/finish-reviewer.md b/vendor/interface-design/impeccable/skills/impeccable/reference/degraded/finish-reviewer.md index 06f257d..b5023c1 100644 --- a/vendor/interface-design/impeccable/skills/impeccable/reference/degraded/finish-reviewer.md +++ b/vendor/interface-design/impeccable/skills/impeccable/reference/degraded/finish-reviewer.md @@ -1,5 +1,5 @@ -This harness has no subagent capability, so you are running this role inline. Step fully out of the work you just finished, adopt only this file's instructions for the pass, and disclose the substitution in one line when you report. Where the text below addresses a parent agent, you are both parties: produce the full output contract first, then act on it yourself. +This harness has no subagent capability, so you are running this role inline. Step fully out of the work you just finished, adopt only this file's instructions for the pass, and disclose the substitution in one line of your closing message to the user, outside the output contract. Where the text below addresses a parent agent, you are both parties: produce the full output contract first, then act on it yourself. # Impeccable Finish Reviewer diff --git a/vendor/interface-design/impeccable/skills/impeccable/reference/degraded/manual-edit-applier.md b/vendor/interface-design/impeccable/skills/impeccable/reference/degraded/manual-edit-applier.md index 2bb6b4f..7f2bdb6 100644 --- a/vendor/interface-design/impeccable/skills/impeccable/reference/degraded/manual-edit-applier.md +++ b/vendor/interface-design/impeccable/skills/impeccable/reference/degraded/manual-edit-applier.md @@ -1,5 +1,5 @@ -This harness has no subagent capability, so you are running this role inline. Step fully out of the work you just finished, adopt only this file's instructions for the pass, and disclose the substitution in one line when you report. Where the text below addresses a parent agent, you are both parties: produce the full output contract first, then act on it yourself. +This harness has no subagent capability, so you are running this role inline. Step fully out of the work you just finished, adopt only this file's instructions for the pass, and disclose the substitution in one line of your closing message to the user, outside the output contract. Where the text below addresses a parent agent, you are both parties: produce the full output contract first, then act on it yourself. # Impeccable Manual Edit Applier diff --git a/vendor/interface-design/impeccable/skills/impeccable/reference/new-work.md b/vendor/interface-design/impeccable/skills/impeccable/reference/new-work.md index ecddd65..959e123 100644 --- a/vendor/interface-design/impeccable/skills/impeccable/reference/new-work.md +++ b/vendor/interface-design/impeccable/skills/impeccable/reference/new-work.md @@ -48,7 +48,7 @@ The script deals three of your structures; the dice pick which three reach the u 4. Run `"/scripts/impeccable" concept-seed --scope direction --mode ` and follow what it prints. No substitute, no skip: on a new or replacement world, writing artifact code before this script has run and its assignment is acknowledged is a contract violation, whatever the harness, the model, or the time pressure; the roll is what keeps every run from converging on the category default. The script assigns the direction to build and deals catalog challengers. Fuse each challenger before judging it: the challenger supplies the form and its system grammar, the product supplies every fact, clarity wins conflicts. Weigh fused challengers against the assigned direction on exactly two axes, audience identification and product clarity. Losing to strong grounded material is a valid outcome; beating a thin or tool-monoculture list is the point. Close with a verdict per challenger, decided before any borrowing: wins (beats the assigned direction on both axes; becomes the build candidate), competitive (holds one axis; stays a full alternate), or declined (loses both). A declined challenger is not spent: name the one discipline of its system the assigned direction lacks, and raise the assigned direction to match before presenting it. A donation transfers ambition and system discipline (a palette's total commitment, a grid's density courage, a form's structural honesty), never the challenger's clothes. A component treatment, such as a button's shadow or a display face, is clothes, not discipline; a lifted motif is a costume note, not a raise, and one world owns the page. Write each raise into the presented direction as its own line, named for its donor; a raise nobody can read did not happen. 5. Present one direction, fully committed and already raised by the hand it beat, raises visible as named lines: world, first viewport, visitor path, signature interaction, cross-surface reach, honest risk. Route each challenger by verdict: winning and competitive challengers are full alternates with their QUALITY BAR cards and one-line case; declined challengers render demoted, compact and quiet, each carrying its verdict and what the direction kept from it, never full-size, never silently dropped, still adoptable on request. The verdict informs the user's choice, never pre-empts it; the demoted row is the hand's proof of judgment. A hand holds at most three full-card challengers: when the roll deals more, the three strongest join and the rest wait in the re-roll pool, noted in one line; dropping a challenger from the hand itself takes a named product-truth failure, disclosed. The hand's size follows the verdicts, never a number the user named: when the weighing yields fewer full cards than asked, deliver the honest hand, say in one line why it is smaller, and offer a re-roll to deal candidates that could earn full cards. A declined challenger is never promoted to fill a slot; a promoted card looks exactly like a competitive one, so the promotion leaves no trace. The standing exit does not count toward a requested number once the user has seen and rejected it; it stays present as the quiet door. Add one card for your own top-ranked grounded candidate when it is not the assigned direction, kicker IMPECCABLE’S PICK, same anatomy as every card, with an honest risk line naming its familiarity when true: the strongest grounded direction is often where most runs in this category land, and the user deciding that trade is the point of showing it. Familiar and effective is a legitimate destination, not a failure of nerve; the pick card and the standing exit serve it at two depths. One pick card, never two, never a ranked list: a lineup of your candidates hands selection back to a taste function and invites the safest card. The pick never takes the lead position; when the dice assign your top candidate there is no pick card, and the assigned card notes it topped your list. Add re-roll with an optional one-line steer, in three registers: plain (a fresh hand, same spread), safer (your remaining conventional grounded candidates plus the canon against named competitors), bolder (foreign forms only, at full commitment). The register is the user's steering on the familiar-to-bold axis, never yours to pre-select; when the answer carries one, re-run the seed with `--register ` and the next `--reroll` round, and follow what it prints. A user saying "bolder" or "safer" while a direction round is open means these registers, never the bolder or harden commands. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool, whose option list carries the assigned direction, the pick, the winning and competitive challengers, and the standing exit last; declined challengers fold into the assigned option's description as their kept lines, so the raise survives the text channel. -The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it (the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path), convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. Record a standing preference as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. Re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Resolve collisions field by field: preserve every user- or brief-pinned constraint. In dimensions the brief leaves open, the assignment still binds through its topology, controls, state vocabulary, and ritual; when only its materials conflict with a pinned visual direction or PRODUCT.md brand commitment, translate that material expression and name the translation in the presented direction. A look mismatch is not grounds to re-roll. Present the decision visually: write an options payload with the assigned direction leading, its raised lines included; the pick card when one exists; the dealt challengers as alternates with their QUALITY BAR cards, verdicts, and kept lines; re-roll with its safer and bolder registers; steer; canon enabled; and `buildPath` carrying the recorded default with `toggle: true` whenever image generation exists (details in the build-path paragraph below). A degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Every option declares its identity: `kind` is `assigned`, `pick`, `challenger`, or `grounded` (another of your own grounded directions, such as each card of a safer hand's lineup, any number of them), and each challenger carries `catalogId`, the SOURCE ID concept-seed printed for its world, never the card's own id; `--start` and `--update` refuse a direction or surface payload without them, and the page reports the choice from them. Give every card the same anatomy: thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (`--schema` prints the exact shape); the page renders identity from these fields, demotes declined challengers to their row on its own, and a challenger's catalog image rides as labeled inspiration, never the promise of the build. Author `canonCard` too: the category standard as one honest card, same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `"/scripts/impeccable" serve-question --start --payload ` (`--schema` first for the payload shape). It daemonizes, prints the page URL and a key, and exits; open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. An ANSWER of `{"optionId":"reroll"}` keeps the server alive and the page open on a loading hand: rerun concept-seed with the same `--scope` and `--mode` plus `--from --reroll ` (1 on the first re-roll, counting up), build the next payload, deliver it with `--update --key --payload `, then return to `--wait` on that key. Never `--start` a second server or fall back to chat here: either strands the open tab on a hand that never arrives. Exit 4 means the page closed unanswered: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may run the script without `--start` and let it auto-open and block. A harness that cannot hold a blocking `--wait` (it detaches long-running commands without ever returning their output, or reaps a backgrounded child) takes the structured tool without starting the page and discloses the substitution in the first reply, so the tab is never left unanswered. A shell that hands back a still-running command as a session you can poll holds the wait: poll that session until it exits, and rerun `--wait` only after it exits without an answer. Never predict any other fallback: run the script, and only exit code 2 from starting it, or that wait check before starting, routes the decision to the structured tool; those are the fallback, never an error to retry. concept-seed closes every roll with this procedure, condensed, as a PRESENTATION block that names the recorded build path. +The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it (the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path), convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. Record a standing preference as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. Re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Resolve collisions field by field: preserve every user- or brief-pinned constraint. In dimensions the brief leaves open, the assignment still binds through its topology, controls, state vocabulary, and ritual; when only its materials conflict with a pinned visual direction or PRODUCT.md brand commitment, translate that material expression and name the translation in the presented direction. A look mismatch is not grounds to re-roll. Present the decision visually: write an options payload with the assigned direction leading, its raised lines included; the pick card when one exists; the dealt challengers as alternates with their QUALITY BAR cards, verdicts, and kept lines; re-roll with its safer and bolder registers; steer; canon enabled; and `buildPath` carrying the recorded default with `toggle: true` whenever image generation exists (details in the build-path paragraph below). A degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Every option declares its identity: `kind` is `assigned`, `pick`, `challenger`, `grounded` (another of your own grounded directions, such as each card of a safer hand's lineup, any number of them), or `dealt` (a surface round's dealt structures other than the lead), and each challenger carries `catalogId`, the SOURCE ID concept-seed printed for its world, never the card's own id; `--start` and `--update` refuse a direction or surface payload without them, and the page reports the choice from them. Give every card the same anatomy: thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (`--schema` prints the exact shape); the page renders identity from these fields, demotes declined challengers to their row on its own, and a challenger's catalog image rides as labeled inspiration, never the promise of the build. Author `canonCard` too: the category standard as one honest card, same anatomy; the page keeps it subordinate, and the counterweights still bind you. Run `"/scripts/impeccable" serve-question --start --payload ` (`--schema` first for the payload shape). It daemonizes, prints the page URL and a key, and exits; open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. An ANSWER of `{"optionId":"reroll"}` keeps the server alive and the page open on a loading hand: rerun concept-seed with the same `--scope` and `--mode` plus `--from --reroll ` (1 on the first re-roll, counting up), build the next payload, deliver it with `--update --key --payload `, then return to `--wait` on that key. Never `--start` a second server or fall back to chat here: either strands the open tab on a hand that never arrives. Exit 4 means the page closed unanswered: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may run the script without `--start` and let it auto-open and block. A harness that cannot hold a blocking `--wait` (it detaches long-running commands without ever returning their output, or reaps a backgrounded child) takes the structured tool without starting the page and discloses the substitution in the first reply, so the tab is never left unanswered. A shell that hands back a still-running command as a session you can poll holds the wait: poll that session until it exits, and rerun `--wait` only after it exits without an answer. Never predict any other fallback: run the script, and only exit code 2 from starting it, or that wait check before starting, routes the decision to the structured tool; those are the fallback, never an error to retry. concept-seed closes every roll with this procedure, condensed, as a PRESENTATION block that names the recorded build path. When image generation exists, every card also declares a `comp` path under `.impeccable/mocks/decision/`, the canon card included. Where the harness sandboxes its shell, start the page through the least-sandboxed command path it offers: a sandboxed shell cannot bind the board's port, and the first-attempt failure costs a retry every session. Serve the page first, then produce the comps; the page shimmer-waits per slot and the user may answer before they land. Load [visualize.md](visualize.md) before writing the first decision comp prompt, not at the comp round: each card's image is that direction's north-star comp at full fidelity under its comp discipline and the MODE RULES block concept-seed printed, and a prompt written before reading them misses every rule there: the requested surface as the real page, its first viewport and the start of the section below it, held like every comp to visualize.md's rule that a template skeleton stays the template whatever world paints it (your own pick included: a safe layout under a world skin is the template), a short prompt in visualize.md's shape, real product name and real content, no invented commercial claims, in that card's own palette, type character, and material world, committed all the way. Once a round declares comps it has not generated yet, `impeccable serve-question --start` (and `--update`, and a flip to comp) prints a `NEXT read /visualize.md` line; read that file then, before the first prompt. `--wait` prints `COMP SIDECAR MISSING` for any landed decision comp with no prompt sidecar (the image's full file name plus `.json`: `a.png` gets `a.png.json`); write the missing sidecar before you continue. `COMP STALE` names a file an earlier round left at one of this hand's slots; regenerate it there. Generation takes the same time at any fidelity, so an unfinished draft pays comp cost for draft quality; fairness between cards is equal fidelity in each card's own grammar, one surface, one aspect, never shared unfinishedness. The frame's aspect is the surface's own: portrait at device viewport for a native app or mobile-first surface, landscape for desktop web; the decision page adapts to either, and a phone screen comped landscape is a broken frame, not a neutral default. Produce in reading order, the assigned card, then the pick, then the full-card hand, then canon, each file written with its prompt sidecar the moment it is done, so a re-roll's spend front-loads onto the cards read first; declined challengers get no comp, their catalog thumb is their face. With parallel subagents, fan out one agent per card: each spawn is the shipped asset producer with a single-comp packet, that card's fields, its world's card images when the NEXT line names them, PRODUCT.md, the shared frame, the path to visualize.md for its comp discipline, the COMPS section of the MODE RULES block copied verbatim (the subagent never sees the roll's output), and the card's declared path, up to four in flight. Regenerate inline any slot still empty when its agent returns; drop without ceremony any slot still empty when the user answers. No other supervision is owed. Without parallel subagents, generate in the main thread after serving, same order, and let the harness's own generation display carry the progress; the wait for the answer follows the last file. The chosen card's comp is not spent by the choice: comp-led, it enters the comp round as compositional option one; code-led, it returns at the finish review as the critique reference, what the image dared that the build did not. Unchosen comps stay in `.impeccable/mocks/decision/` as the round's spent hand; they carry no approval and imply none. With no image generation, cards carry their identity in palette chips and facts, and that page is complete, not a lesser version; the page then also demotes every challenger's catalog art to a labeled thumbnail on its own, because salience must encode the verdict, never the accident of which cards have images. @@ -149,6 +149,6 @@ A rebuild and a fix round share one asset rule: a raster either round creates or Report the final verdict under the reviewer's own disposition word and at its actual scope. A verdict pass scores the listed fixes and nothing else: "the reviewer scored all three fixes resolved" is a claim it supports, "no material issues remain" is not. A table with open material findings is never announced as a pass, never softened, and never dressed as whole-surface approval when only a fix list was scored. When the user answers a ship with evidence against it, their own screenshot, a named mismatch with the comp, that evidence outranks every capture you made: put their material in the packet and spawn a fresh reviewer for a new full review. Patching inline and self-certifying is how a rejected page ships twice. -After the last correction, spawn the shipped documenter, `impeccable-documenter` (`impeccable_documenter` in codex), with the project root, artifact path, direction contract, PRODUCT.md, [document.md](document.md), and write boundary. Without subagents, load [degraded/documenter.md](degraded/documenter.md) and [document.md](document.md) before writing. Verify the outcome: new worlds and approved system changes require token-bearing DESIGN.md **and** `.impeccable/design.json`, not prose alone. Ordinary extensions compare the finished build against the incumbent system, preserve its files, and report the evidence checked; report pre-existing drift without repairing it unasked. Recheck after later edits. Finish only when review and documentation are complete. +After the last correction, on a new or replacement world or an approved system change where a DESIGN.md already exists at the write boundary, collect the choice [document.md](document.md) requires: show the user the existing file. Ask with the AskUserQuestion tool and wait for the answer before continuing. Offer refresh, overwrite, or merge. Then spawn the shipped documenter, `impeccable-documenter` (`impeccable_documenter` in codex), with the project root, artifact path, direction contract, PRODUCT.md, [document.md](document.md), write boundary, and that choice; the documenter cannot ask and writes nothing over an existing DESIGN.md without it. Without subagents, load [degraded/documenter.md](degraded/documenter.md) and [document.md](document.md) before writing. Verify the outcome: new worlds and approved system changes require token-bearing DESIGN.md **and** `.impeccable/design.json`, not prose alone. Ordinary extensions compare the finished build against the incumbent system, preserve its files, and report the evidence checked; report pre-existing drift without repairing it unasked. Recheck after later edits. Finish only when review and documentation are complete. On a comp-led build, record the final review disposition with `"/scripts/impeccable" build-phase finish --disposition ` before the final response. A refused `ship` is an unfinished build; report the outstanding phase with the verdict. Any edit after the last captured gate, a fix for a hook finding after `ship` included, voids that capture: run `finish --disposition ship` again before you stop, which rechecks the current files against the gate and names any recapture it needs. Never record `fix` to end a run over an open gate: take the route the gate names, and when the user leaves it open, report that phase as open. diff --git a/vendor/interface-design/impeccable/skills/impeccable/scripts/impeccable b/vendor/interface-design/impeccable/skills/impeccable/scripts/impeccable index b71d893..0b31c9e 100755 --- a/vendor/interface-design/impeccable/skills/impeccable/scripts/impeccable +++ b/vendor/interface-design/impeccable/skills/impeccable/scripts/impeccable @@ -74,6 +74,9 @@ hash_file() { } dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +# VS Code syncs a plugin without the executable bit. Its hooks run this through +# sh, so restore the bit for the commands the skill runs directly (#1249). +[ -x "$0" ] || chmod +x "$0" 2>/dev/null || : manifest="$dir/engine-checksums.txt" # What the binary needs to know about its home: the skill directory (for