尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

Penpot 前端 UI 规范与设计系统:CLJS 组件、SCSS 样式与 i18n 工程实践指南

发布时间:2026/9/8 20:39:29

资讯中心
01
ARTICLE

Penpot 前端 UI 规范与设计系统:CLJS 组件、SCSS 样式与 i18n 工程实践指南

Penpot 前端 UI 规范与设计系统:CLJS 组件、SCSS 样式与 i18n 工程实践指南
Penpot 前端 UI 规范与设计系统CLJS 组件、SCSS 样式与 i18n 工程实践指南【免费下载链接】penpotPenpot: The open-source design platform for Product teams that need scalable collaboration.项目地址: https://gitcode.com/GitHub_Trending/pe/penpot本篇技术指南以 Penpot 仓库中记录的Frontend UI Conventions and Style System为核心系统梳理 Penpot 前端Rumext/CLJS SCSS React/Vite 共享包在组件编写、样式设计、可访问性、国际化、性能与代码组织上的工程约定。读者读完可以掌握如何为编辑器/仪表盘编写符合规范的可复用 CLJS 组件、如何借助stl宏与 CSS Modules 维护低特异性样式、如何正确接入 i18n 翻译流程以及如何决定一段 UI 代码该放进 CLJS 应用还是frontend/packages/ui。一、背景Penpot 前端的技术底座Penpot 的前端位于仓库的 frontend 目录采用一套“混合但分层清晰”的技术栈CLJS 应用层使用 Rumextv2命名空间rumext.v2驱动的 React 封装承载编辑器editor、仪表盘dashboard、查看器viewer等完整工作流逻辑SCSS/CSS Modules通过构建期生成的.css.json映射实现类名哈希与作用域隔离设计系统DS目录位于 frontend/src/app/main/ui/ds存放与业务状态无关的基础组件按钮、输入框、弹窗、排版等共享 React 包位于 frontend/packages/ui是框架中立的 React/Vite 组件库与 CLJS 应用状态Potok/Rumext解耦。下面各节把这份约定文档展开成可直接指导开发的工程规范并补充对应源码证据。二、CLJS 应用 UI 的组件编写规范2.1 组件命名与位置约定主应用组件统一放在frontend/src/app/main/ui*命名空间树下。约定如下组件变量名以*后缀命名如button*、heading*使用Rumextmf/defc定义调用点统一写作[: component* props]形式。这一约定在 button.cljs 中有真实体现(mf/defc button* {::mf/schema schema:button} [{:keys [variant icon children class on-ref to type] :rest props}] (let [variant (d/nilv variant primary) element (if to a button) internal-class (stl/css-case :button true ...)] [: element props (when icon [: icon* {:icon-id icon :size m}]) children]))2.2 清晰的组件所有权children 优先slots 谨慎组件应当具有清晰的“所有权”边界普通组合用children只有当需要分隔出独立“属地”owned regions时才使用slot 式 props不要样式化或结构性操作组件自身未实例化的子 DOM——例如一个容器组件不该去假设孙组件的内部结构。这保证父子组件之间通过 props/children 契约解耦CSS Modules 的类名也不会跨组件泄漏。2.3 class prop 的接收与合并mf/spread-props当调用方合理地需要布局/定位定制时组件应接收并合并classprop并使用 Rumext 的mf/spread-props进行合并。原因是mf/spread-props会继续走 Rumext 的 prop 变换管线例如:class→className保证工具链行为一致。源码中可见其典型用法button.cljsprops (mf/spread-props props {:class [class internal-class] :href to :type (d/nilv type button) :ref (fn [node] (when on-ref (on-ref node)))})这里外层传入的class与组件内部计算出的internal-class合并同时:href、:type、:ref也一并透传——to存在时渲染为a否则为button是语义化与可定制性的平衡范例。2.4 规避以?结尾的布尔 prop避免把布尔 prop 命名为?结尾如visible?。这类符号无法干净地翻译成 JavaScript props在跨 CLJS/JS 边界或与共享 React 包互操作时会产生问题。当 JS 侧的 truthiness/语义重要时应使用类型提示^boolean显式标注。2.5 私有组件的本地约定::mf/private true需要拆分的大型组件可以拆成若干小的私有组件::mf/private true是本地约定。仓库内大量使用例如 register.cljs 中的多处以{::mf/private true}声明私有子组件避免污染公共导出面。三、样式体系stl 宏、CSS Modules 与设计系统 Token3.1 首选同地共置co-located的 SCSS 模块每个组件与其.scss文件放在一起如 button.cljs 旁的 button.scss。通过app.main.style/stl宏族stl/css、stl/css-case、stl/css*从 CLJS 侧引用类名。stl的实现位于 style.clj它根据当前命名空间所在文件计算 CSS Modules 前缀读取同名的.css.json映射做真实类名查找然后把多个类名以空格拼接。关键点在于命名空间关键字如:global/two-row明确表示“不做 Modules 查找直接输出原名”普通关键字会被加上哈希前缀后输出css/css-case还支持按布尔条件可为boolean、表达式或运行时求值决定是否输出某个类。stl/css-case的典型条件类写法见 button.cljs(stl/css-case :button true :button-link (some? to) :button-primary ( variant primary) :button-secondary ( variant secondary) :button-ghost ( variant ghost) :button-destructive ( variant destructive))3.2 保持低 CSS 特异性避免嵌套选择器除非确实在定位组件自身实例化的元素由于 CSS Modules 已经杜绝类名碰撞不需要靠高特异性或深嵌套来“占位”需要跨浏览器关注态时优先伪类:hover、:focus-visible而非深层 DOM 选择器。3.3 优先 CSS 逻辑属性方向性间距/布局优先用CSS 逻辑属性例如padding-inline-start、margin-block-end、inset-inline。这样能天然支持 RTL从右到左排版。物理width/height在语义更清晰的场景如图标尺寸仍然允许。3.4 一律使用设计系统 Token禁止硬编码与废弃变量间距、边框、固定尺寸、颜色、排版必须使用命名好的设计系统变量/Tokens禁止硬编码px/rem使用已废弃的resources/styles/common/refactor/spacing.scss旧变量。设计系统 SCSS Tokens/Mixins 集中在 frontend/src/app/main/ui/ds顶层包含文件承载的 Tokencolors.scss颜色 Token如--color-accent-primaryspacing.scss间距尺度_sizes.scss尺寸 Token如$sz-32_borders.scss边框宽度$b-1、圆角$br-8_utils.scss工具函数如px2remmixins.scss排版等复用 Mixinsz-index.scss层叠级别elevations.scss阴影/抬升例如 _buttons.scss 的组合形态use ds/_borders.scss as *; use ds/_sizes.scss as *; use ds/_utils.scss as *; %base-button { height: var(--button-height); border: $b-1 solid var(--button-border-color); border-radius: $br-8; ... :focus-visible { outline: var(--button-focus-inner-ring-color) solid #{px2rem(2)}; } }3.5 组件多视觉态优先局部 CSS 自定义属性当组件拥有多种视觉状态hover、active、disabled、不同 variant时使用组件局部的 CSS 自定义属性custom properties驱动主题而不是一次性 Sass 变量。上述按钮基座即以此设计先声明--button-bg-color、--button-fg-color、--button-hover-*等占位再在伪类中做状态切换variant 具体颜色在%base-button-primary等扩展片段里用--color-accent-*Token 赋值。这样一条规则即可覆盖所有状态无需为每个状态写死一套新选择器。3.6 排版优先 DS 排版组件与 Mixins不要在业务代码里写裸文本包裹器或旧排版 Mixins。优先使用设计系统排版组件与排版 Mixinsheading*heading.cljstext*text.cljs底层由 typography.scss 等 DS 排版 Token 支撑。四、可访问性Accessibility约定语义 HTML 优先导航/下载/邮件链接用a动作用button标题层级h1–h6正确所有控件可键盘聚焦原生元素不可用时补 ARIA按 WAI-ARIA APGAuthoring Practices Guide中标准 widget 的模式实现如组合框、标签页、弹窗、tooltip。例如 DS 组件对可切换元素使用aria-pressedbutton.scss 的[aria-pressedtrue]分支纯图标控件必须有可访问名称通过周围文本、aria-label、alt或等价机制提供装饰性图标/图片应向辅助技术隐藏如aria-hidden、空alt。聚焦可见性统一走:focus-visible 自定义 outline。五、i18n 国际化工程约定5.1 翻译解析时机翻译必须在渲染期间或渲染期 memoization中解析绝不能在命名空间加载期load time解析。静态选项列表也应把翻译放入渲染内部的 memo这样切换语言时标签仍能即时更新。5.2 翻译文件与构建翻译文件位于 frontend/translations.po格式如zh_CN.po、es.po翻译改动会打进index.html。翻译字符串没有热重载改完翻译后要刷新浏览器才能看到效果在frontend/下执行pnpm run translations重新生成翻译产物对应 package.json 中的translations: node ./scripts/translations.js实际脚本为 translations.js。5.3 新增语言的两处同步增加一个新的受支持 locale需要同步更新两个文件缺一不可i18n.cljs 的supported-locales如{:label 简体中文 (community) :value zh_cn}_helpers.js 中的langs数组。supported-locales还驱动语言自动探测autodetect会按浏览器语言列表逐一匹配命中则采用否则回退到cf/default-language见 i18n.cljs。运行时切换语言通过set-locale动态加载./translation.locale.js资源i18n.cljs。翻译 APIt/tr支持复数形态app.common.i18n/c标记计数参数数组型词条按计数取单/复数与参数格式化i18n.cljs。六、性能约定昂贵派生数据放对地方存入 refs、memoized selectors 或纯 helper在热渲染路径上优先复用本地已使用的app.common.data.macroshelpers比如依赖更新追踪宏避免手写重复的相等性判断避免在热渲染中新建回调/对象能用命名函数、memoized callback如mf/use-callback、data 属性或预计算的 JS props 对象时就不要内联新建fn与字面量对象防止子组件无意义重渲染重复使用 props/state 时先解构在渲染循环内避免反复 deref 或重复属性访问。七、共享 React UI 包frontend/packages/ui7.1 包的定位frontend/packages/ui 是共享的 React/Vite 包含package.json、vite.config.mts、TS 配置与 Storybook 配置。它相对 CLJS 应用状态保持framework-neutral可复用原语只有不依赖 Potok/Rumext 应用状态时才放进这里它的消费场景是“无需 Penpot 应用状态即可渲染”的展示型组件。7.2 样式的发布路径包样式通过包的构建产出并拷贝到frontend/resources/public/css/ui.css该行为在 vite.config.mts 中配置。因此“共享样式过期”绝大多数时候是构建产物artifact未刷新的问题而不是源码问题——遇到 UI 表现与源码不一致先重新构建包。7.3 Storybook 是主要视觉验证工具Storybook 是共享 UI/包行为的主要视觉调试载体。包与组件测试构建/运行命令用记忆入口mem:frontend/ui-packages-text-editor-workflow。DS 组件的每个原语通常同时提供CLJS 实现如 button.cljsCSS Modulebutton.scssStorybook storybutton.stories.jsx可选 MDX 文档如 buttons.mdx。八、代码该放哪里位置选择决策场景放置位置依据编辑器/仪表盘/查看器工作流逻辑CLJS 应用命名空间贴近所属 feature依赖 Potok/Rumext 应用状态与画布状态可脱离 Penpot 应用状态消费的可复用展示原语frontend/packages/uiframework-neutral使用 DS 的 CLJS 设计系统组件frontend/src/app/main/ui/ds新 DS 组件需四件套实现、CSS module、Storybook story、可选 MDX 文档文本编辑内部逻辑属 JS 编辑器包行为frontend/text-editorJS 包内维护共享文本数据模型行为走mem:common/text-subtleties**新增 DS 组件第 3 行**还要从 frontend/src/app/main/ui/ds.cljs 导出并使用JavaScript-friendly 名称。该文件以mf/object构造一个default导出对象如:Button button*、:Heading heading*、:Input input*…用于 Storybook 与跨语言消费。九、变更后的验证清单按改动范围选择验证手段CLJS 应用 UI跑mem:frontend/testing与mem:frontend/compile-diagnostics当行为依赖 store 或画布状态时补浏览器/REPL 实机检查共享 UI 包跑包构建 Storybook/组件测试文本编辑器跑 frontend/text-editor 的测试若涉及 render-wasm 产物需刷新/拷贝 WASM 产物后再验证。这一“按层选择验证手段”的原则与上文“按层组织代码”的原则一一对应改动落到哪一层就用哪一层的测试与工具链收口确保视觉与行为双回归都被覆盖。十、总结Penpot 的前端约定可以概括为一句闭环组件有清晰属地主权、样式低特异性且 Token 化、翻译渲染期解析、共享 UI 与应用状态解耦、新增 DS 组件四件套齐备、按层验证。实践中最容易踩坑的三点忘记类名走stl宏破坏 CSS Modules 前缀映射、在命名空间加载期解析翻译切语言不生效、改动frontend/packages/ui后没重建产物ui.css陈旧。对照本文规范与对应的源码实现即可写出与 Penpot 主前端代码风格一致、可无障碍维护的 UI 代码。【免费下载链接】penpotPenpot: The open-source design platform for Product teams that need scalable collaboration.项目地址: https://gitcode.com/GitHub_Trending/pe/penpot创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。