Simulator Styles logo

Simulator Styles

Organization
corezoid
simulator-styles

Simulator.Company Smart Form (CDU) STYLING specialist — authoring complex Less/CSS for Smart Forms: theme tokens, page/form/section layout, component re-skinning, reusable style patterns, responsive and design-system approaches. Use when the user wants to STYLE or RESTYLE an existing Smart Form / CDU app — change its look, build a theme, style a table / sidebar / modal / form, add a design system, fix spacing/colors/fonts, or apply a complex visual design. This skill owns the `style` / `styles/` layer; it reuses the Smart Form tools (pullSmartForm / pushSmartForm / deploySmartForm) but does NOT create form templates — for the data-schema form template use `simulator-forms`, and for page layout / viewModel / backend logic use `simulator-smart-forms` / `simulator-smart-forms-logic`. Activate on: "style a smart form", "CDU styles", "theme the form", "restyle", "custom CSS/Less for the app", "style the table/sidebar/modal/button", "design system for the smart form", "make it look like …", "застилізувати смартформу", "стилі CDU", "тема для форми", "кастомний CSS", "оформити таблицю/ сайдбар/модалку", "застилизовать смартформу", "стили CDU", "тема формы", "кастомный CSS", "оформить таблицу/сайдбар/модалку".

Overview

Publishercorezoid
Repositorysimulator-ai-plugin
Skill namesimulator-styles
Stars
61
Forks
13
Bundled files
Instructions only
LicenseMIT
Links
  • Markdown instructions

    A SKILL.md file the model loads on demand, so it only costs tokens when a request actually matches.

  • Works with any LLM

    AI skills are plain Markdown, not provider-specific code, so this works with GPT, Claude, Gemini, Grok, or a local model.

  • Self-contained

    Everything the model needs lives in the instructions — no extra files to sync.

  • Open source

    Published by corezoid on GitHub. Read the source before you install it.

Installation

Install the Simulator Styles AI skill in TypingMind to use it with any LLM, or drop it into another agent that reads SKILL.md.

1

Install in TypingMind

TypingMind installs a skill straight from its GitHub folder — it reads SKILL.md, bundles the resource files, and stores the result locally.

  1. Open the app and go to Plugins → Skills.
  2. Choose "Install from GitHub".
  3. Paste the skill folder URL below and confirm.
    https://github.com/corezoid/simulator-ai-plugin/tree/main/plugins/simulator/skills/simulator-styles
  4. Enable the skill in any chat where you want it available.
Plugins → Skills → Add skill → From GitHub URL, then paste the folder URL and press Continue.
2

Install in another agent

Any agent that reads the Agent Skills format can use this skill — copy the folder into that agent's skills directory.

Claude Code — .claude/skills
git clone --depth 1 https://github.com/corezoid/simulator-ai-plugin.git /tmp/simulator-ai-plugin
mkdir -p .claude/skills
cp -r /tmp/simulator-ai-plugin/plugins/simulator/skills/simulator-styles .claude/skills/simulator-styles
Restart Claude Code after copying so it picks up the new skill.

Use it in TypingMind

Enable Simulator Styles in any TypingMind chat and the model takes it from there. Its name and description sit in the system prompt, and the moment a request matches, the model loads the full instructions itself — you never invoke it by hand, and it costs no tokens until it is actually used.

The model loads Simulator Styles on its own as soon as a request matches it.

Works with any AI model

AI skills are plain Markdown instructions rather than provider-specific code, so Simulator Styles is not tied to the model it was written for. Install it once in TypingMind and use it with GPT-5, Claude, Gemini, Grok, DeepSeek, Mistral, Llama, or a local model you run yourself — all on your own API keys.

  • Loaded only when it is needed

    The system prompt carries just the name and description. The instructions are fetched on the first matching request, so an idle skill costs nothing.

  • Switch models mid-chat

    Because the skill is instructions rather than code, changing model does not break it — the next model reads the same SKILL.md.

Skill instructions

This is the SKILL.md content the model loads. Read it before installing — a skill is instructions your model will follow.

Simulator.Company Smart Form STYLING specialist

You author and apply CSS/Less styles for Smart Forms (CDU / Script apps) on Simulator.Company. Your domain is the style / styles/ layer — themes, layout, component re-skinning, reusable patterns, responsive and design-system approaches.

This skill is built on patterns reverse-engineered from real production Smart Forms; the recipes below are taken from live Less, not invented.


Scope — what this skill owns (and what it does NOT)

ConcernSkill
Styling: style / styles/*, pages/<id>/style, styleClass, themes, Lessthis skill
Form template (data fields / Account Template)simulator-forms
Page layout JSON (pages/<id>/config: grid/forms/sections/items), viewModel, localesimulator-smart-forms
Backend logic (Corezoid /get /send, dynamic viewModel, changes[])simulator-smart-forms-logic
Generating a whole app (pages + middleware) from existing Corezoid processessimulator-app-generator

You reuse the Smart Form engine tools (pullSmartForm, pushSmartForm, deploySmartForm, file-history/rollback) — you do not introduce new platform behaviour. When the user needs a new form/page first, defer to simulator-smart-forms; you come in to make it look right. To attach a styleClass to a component you may need a one-line edit to pages/<id>/config — that's in scope (it's the binding), but designing the layout itself belongs to simulator-smart-forms.


How Smart Form styling works (the model)

  1. One Less stylesheet, compiled per save, scoped to .cdu-page. Whatever you write is wrapped in .cdu-page { … } at serve time, so & = the page root and your styles can't leak out. Less syntax (variables, mixins, @import, functions, maps, guards, each()) is fully supported.

  2. File organization — and where each rule goes. Two layouts compile identically; prefer the modular styles/ folder for anything non-trivial:

    • Legacy single file — a root style file holds everything (big forms: Admin Panel, CMS, LMS).
    • Modular styles/ — styles/index is the entry point and mainly just @imports partials, in cascade order:
      less
      // styles/index — the entry/manifest; imports in cascade order
      @import "colors_fonts";   // tokens FIRST (everything below + page styles inherit them)
      @import "init_styles";    // then platform resets + project-wide component defaults
      // … any other shared partials (mixins, shared components) …
    • Page styles — pages/<id>/style is auto-appended after the main stylesheet (so it wins the cascade) and inherits all root variables/mixins (no @import needed). The platform saves it as text/css automatically. It is for that page's exceptions only.
    FileWhat belongs here
    styles/indexJust @imports (the entry/manifest), in cascade order.
    colors_fontsColor + font tokens (@color_*, @font_*, @font-face). Define them here because everything else — including page styles — inherits them.
    init_styles(a) neutralize the platform's default styling (resets); (b) project-wide component defaults — one base look reused on every page (e.g. a .button skin).
    pages/<id>/styleOnly the page's differences from the shared styles — page layout + one-off component tweaks. Never define tokens here (they live in colors_fonts).
  3. styleClass is the binding contract between layout JSON and CSS. Every grid / form / section / item in pages/<id>/config may carry a styleClass; your CSS targets that class.

    • Static for structure: "styleClass":"main_table".
    • Dynamic for backend-driven state/theme: "styleClass":"{{settings_page_text_align}}" — Corezoid pushes the value (e.g. text_align_right, active_sidebar_btn) to switch styling.
  4. Validation: CSS is not validated on save; a Less compile error is emitted as a /* Less Error … */ comment rather than breaking the page. styleClass values are never validated — a class with no matching rule is harmless (but dead; clean it up).

  5. Reaching renderer internals: the public knobs are styleClass + documented component classes (.button, .edit, .select, .table, .check, …). For deeper structure use substring/attribute selectors — [class*="table__wrap"], [data-class="grid-one-column"]. Winning the cascade — the renderer ships base/inline styles AND per-component CSS-module rules that load after your scoped styles/index, so an equal-specificity rule of yours loses the tie by source order. Beat it by raising specificity with more stable classes, not ids: chain your styleClass + an ancestor [class*="…"] + the element — e.g. .book-table td.bc-cover-cell .file img (0,3,2) beats the component's .file__item__hash img (0,2,1). Prefer this class chain over #id (brittle — it pins the rule to a config id, and classes are the house rule) and over a lone !important (it only beats non-!important; in an !important vs !important fight specificity still decides). A doubled class (.x.x) is a last resort. See the verified per-component DOM map below for exact hooks (edit, select, multiselect, radio, button, row__<name>).

    The cascade math, with real numbers (measured against a live renderer build — dump the defaults yourself to confirm, see Workflow §"dump the defaults"):

    • Your stylesheet is auto-wrapped in .cdu-page, so a bare single-class rule you write is already 0,2,0 (.cdu-page .myClass) and beats a bare renderer default .hashed (0,1,0) for free — most overrides need nothing more.
    • BUT the renderer theme-scopes ~⅓ of its rules (.theme-light .x / .theme-dark .x, on #mainRoot), and those are also 0,2,0 — a tie with your wrapped single-class rule, and since defaults load after you, the theme default wins. Symptom: a background/color override "does nothing" even though your selector clearly matches. Beat it with an extra class (0,3,0) or !important.
    • ~1 in 5 default rules already use !important (heavy on active/selected/checked states and theme colors). Matching them with your own !important is normal here, not a smell — a lone !important still loses to a default !important of higher specificity, so pair it with a solid class chain.
    • Slots own their own background/padding. Containers like .section__content, the sidebar slots (.sidebar__header/__content/__footer), and .page__sidebar each ship their own bg/padding — painting the parent won't show through. Override the slot, not its ancestor.

Authoritative component-class + CSS reference: the "CSS styling" tag in the CDU swagger and $CLAUDE_PLUGIN_ROOT/docs/user-flows/cdu-page-protocol.md.


Rendered DOM map (verified component internals)

Verified against a live control-cdu render. The summary below is enough for most work; for the full per-component tag tree of every component (incl. all 4 table types, page skeleton, overlays) see $CLAUDE_PLUGIN_ROOT/docs/user-flows/cdu-dom-tree-reference.md. Use this to target the right element instead of guessing. Three rules first:

  1. styleClass lands on the component ROOT. E.g. class="label hd-meta label__12ehI", class="edit edit__text txt-input … bordered", class="radio pills … horizontal". So a root hook (.hd-meta, .txt-input, .pills) always hits — but only the outer element.
  2. Internals use hashed CSS-module classes (label__12ehI, f-item-0-2-64, i-label-0-2-70, clickOutside(i)-field-0-2-97) that change between renderer builds. Never target the hash. Reach inside via substring selectors ([class*="radioItem"], [class*="i-icon"], [class*="chip"], [class*="i-edit"]), stable state classes (.checked .disabled .bordered), data-class attributes, or element selectors (input, textarea, label, svg).
  3. row / w grouping yields a STABLE group class .row__<rowName>. An item with "row":"act" renders inside <div class="row row__act row__hash"> whose children are <div class="row__item__…" style="width:…%">. Note w is a relative weight, not a raw percentage: the rendered width is w / Σw across the row (two items at w:50 each get 50%; at w:50 + w:100 they get 33% / 67%). Style the group via .row__act (flex container) and items via [class*="row__item"] — your reliable hook for multi-column rows, progress bars, etc.

Page skeleton & theme (stable structural hooks):

  • Scope: all your CSS is wrapped in .cdu-page (& = page root); per-page hook .cdu-page-<pageId>.
  • Dark mode is a CLASS, not a media query: .theme-light / .theme-dark. In a live render the class sits on the host wrapper #mainRoot (an ancestor of .cdu-page), which is why .theme-dark .… {} works from inside your scoped stylesheet. control-cdu itself also stamps the theme class onto #page (the .cdu-page node) — so verify the placement on your build: if the class is only on the .cdu-page node (not an ancestor), a wrapped .theme-dark .foo won't match and you'd need &.theme-dark .foo. Prefer this class approach over @media (prefers-color-scheme). ⚠️ Caveat: the renderer's light/dark token maps are currently identical — the mechanism is wired but dark mode has no distinct palette yet, so a .theme-dark override is the only way to make dark actually differ today.
  • Grid regions: [data-class="grid-one-column"] / [data-class="grid-two-column"] (+-left/-right); header/footer regions via [class*="gridtwo__header"] / [class*="gridtwo__footer"].
  • Section slots: [data-class="section"] (+ [class*="block__"] for type:"block" cards); inner [class*="section__header"] / [class*="section__content"].
  • Toasts: two [class*="notify"] containers (top inside #page, bottom at #mainRoot end).

Per-component root + key inner hooks (<sc> = your styleClass):

ComponentRoot selector (styleClass here)Key inner hooks / notes
label.label / [data-class="label"]<span> text; BBCode → real tags; align→ .left__/.center__/.right__
divider.dividerempty
edit (all types).edit (+ .edit__<type>).field > input / textarea; states bordered(box)/selected/error; kill box .field{border:none}; help/err [class*="Component-helperText"]
select.select (outer)readonly <input> in [class*="i-edit"] + caret [class*="endAdornment"]; no native <select>
multiselect.multiselectchip field: [class*="chip"] + search <input> via [class*="clickOutside(i)-field"]; not checkbox rows
radio.radio (+ .horizontal for row)options [class*="radioItem"](+.checked/.disabled); hide svg [class*="i-icon"], style label [class*="i-label"] → pills/scales
check.check[class*="f-icon"] svg + <label>; states .checked/.error; native input{appearance:none}
toggle.toggle (+ .left__/.right__)[class*="toggle__button"](+.active) / [class*="i-switch"]
slider.slider (+ .skillBar__)rc-slider: .rc-slider-rail/-track/-handle/-dot; [class*="slider__header"], [class*="slider__min"]/max
otp.otpboxes [class*="otp__edit"] input
phone[data-class="phone"]#countryCode .select + #number .edit input; [class*="phone__items"]
image[data-class="image"]<img> (src proxied via /api/1.0/image); align→ .center__
timer[data-class="timer"]<span> text
comments[data-class="comments"][class*="mes__wrap"], [class*="mes__name"], [class*="mes__content"], avatar [class*="i-avatar"]
carousel.carouselpreview pane [class*="carousel__preview"] (zoom/nav btns), thumb strip [class*="carousel__content__item"](+.active__); give it a full File value for a correct preview (tolerates a missing type — won't error-stub)
buttonwrapper [data-wrapper-for="<id>"] → inner #<id>.button.button__<type><sc> is on the INNER button; text [class*="button__label"]; align via wrapper/.row__<name>; 7 types via .button__<type>
copy[data-class="copy"][class*="copy__container"], icon [class*="i-icon"], label [class*="i-label"]
tab.tab (outer .tab__…)items [class*="tab__item"](+.active/.error); hidden option is absent from DOM
stepper[data-class="stepper"]items [class*="stepperItem"](+.completed__/.active__); label [class*="stepperItem__label"]
mainMenunav.mainMenuitems [class*="mainMenuItem"](+.active); groups <details>/[class*="mainMenuItemGroup"]; data-depth + --depth; badge [class*="i-badge"]
upload[data-class="upload"][class*="upload__box"], corners [class*="upload__corner"], input[type=file] (webcam: trigger div.upload__file__…[role=button], needs extra.accept)
file.fileimage → [class*="file__item"] img; pdf/doc → .pg-viewer/.pdf-viewer; path chosen by mime value.type
attachment[data-class="attachment"]upload ctrl [class*="i-upload"]; chips [class*="fileItemChip"] (text [class*="e-chipText"], remove [class*="e-delFileIcon"])
signature.signature__wrap<canvas>; toolbar [class*="signature__toolbar__clear"]/__save
table[data-class="table"] (+ .table__check/__radio/__group)see Tables below
widget[data-class="widget"]iframe[class*="iframe__"]; [class*="widget__inner"](+.hidden__ until load)
row/w group.row__<rowName>flex container; items [class*="row__item"] (inline width)
draggable (sortable/contentLoop)[data-class="draggable"]grip [class*="draggable__handle"], body [class*="draggable__content"]
notification (toast)none — page root [class*="notify"][class*="notifyItem"] (+ severity [class*="success"]/[class*="error"]/[class*="info"]) → text span[class*="i-title"] + close i[class*="closeIcon"] > svg .fill

Tables (DOM is a real <table>): head cells [class*="table__head"] (sortable col [class*="sortable__"], sort arrow [class*="table__head__icon"]); sticky col → td.sticky-col

  • the head cell's own styleClass; body rows .table-row / [class*="table__row__"] (selected radio row → .active__…); first column for check/radio → [class*="table__check"] / .i-radioItem-…; group title row → [class*="table__row__group"]. Default-table cell mini-components inside <td.table-cell>: file → [class*="table__img"], copy → #<id>--copy[data-class="copy"], check → #<id>--check[class*="table__check"], button → [class*="table__button"] (id <id>--button, not .button).

Table cells are NOT text-only. A default-table cell can be plain text, an image (file cell → real <img>), a button, a copy, or a check — so covers/actions live inside a table (cell JSON shape belongs to simulator-smart-forms; you style via each cell's own styleClass, which lands on its <td>). And plain + head cells render BBCode (verified — see the BBCode matrix below): the text goes into a plain <td>/<div>, so [b]/[color]/[div]… expand there. This is why you can build a status pill in a cell without a button cell: put [div]Label[/div] in the plain cell and style .<cellClass> div.

Overlay sections: modal → backdrop [class*="i-bg"] > box [class*="modal__"] (+ size i-small/i-medium/i-large/i-xlarge ← modalSize), header [class*="section__modal__header"]. float → [class*="float__"] inside [class*="Component-wrapper"] with drag bar [class*="section__header__dragable"] + 8 [class*="Component-resizeHandle"].

⚠️ File-bearing components need a complete value. The file component and a default table's file cell read mime via value.type with no guard — a File missing type throws and the item renders as a [class*="item__error"] stub. (carousel and attachment also read value.type but tolerate it missing — carousel defaults to '' and uses optional chaining — so they won't stub out; still give them the full shape for a correct preview.) Give File values the full shape {fileName, fileSrc, title, type, size}. A stub can also suppress later siblings (a crashing table cell hid the modal/float sections after it). upload[webcam] needs extra.accept. With complete values everything renders statically — no backend needed. Full per-component trees: $CLAUDE_PLUGIN_ROOT/docs/user-flows/cdu-dom-tree-reference.md.

Worked example — radio rendered as pills (the native input is hidden; the svg is the control):

less
.pills { display:flex; flex-wrap:wrap; gap:8px; }
.pills [class*="i-icon"] { display:none; }                 // hide the svg circle
.pills [class*="radioItem"] { border:1px solid @line; border-radius:100px; }
.pills [class*="radioItem"] label { padding:7px 16px; }    // make the whole pill the label
.pills [class*="radioItem"].checked { background:@ink; }
.pills [class*="radioItem"].checked label { color:#fff; }

Worked example — toast notification. Toasts render at the page root (inside .cdu-page, which scopes your CSS), so you reach them structurally — there is no styleClass. For the DOM nesting + the two traps ([class*="i-icon"] matches the close ✕, not a severity icon — don't blanket-hide it; [class*="i-title"] also matches the wrapper i-titleContainer — qualify as span[class*="i-title"]) see the Toasts entry in cdu-dom-tree-reference.md. A reusable skin:

less
[class*="notify"] [class*="notifyItem"] {
  background:@white; color:@ink; border:1px solid @line; border-radius:@r;
  box-shadow:0 8px 28px rgba(0,0,0,.12); padding:14px 16px;
  display:flex; align-items:center; gap:10px;
}
[class*="notify"] [class*="notifyItem"]::before {        // severity dot
  content:""; width:8px; height:8px; border-radius:50%; background:@ink-3; flex-shrink:0;
}
[class*="notify"] [class*="notifyItem"][class*="success"]::before { background:#16a34a; }
[class*="notify"] [class*="notifyItem"][class*="error"]::before   { background:#dc2626; }
[class*="notify"] [class*="titleContainer"] { flex:1; min-width:0; }
[class*="notify"] span[class*="i-title"] { font-size:14px; font-weight:500; color:inherit; }
[class*="notify"] [class*="closeIcon"] { opacity:.45; cursor:pointer; }   // keep the ✕
[class*="notify"] [class*="closeIcon"]:hover { opacity:1; }
[class*="notify"] [class*="closeIcon"] svg .fill { fill:currentColor; }

Hashes (-0-2-64) are version-pinned to the renderer build — substring selectors survive a bump, exact-hash selectors don't. If a restyle suddenly breaks, re-dump the rendered DOM (browser DevTools → copy outerHTML of #page) and re-verify these hooks.


BBCode — where it renders (empirically verified)

A component's text is expanded to HTML by the client (Utils.bbCodeToHtml) only in some fields. The smart-form BBCode set (swagger bbcode tag) is [b] [i] [u] [ul][*] [iurl=…] [url=…] [div] [br] [color=…] [size=…] [bg=…] — note [div] (the only block-level tag) and [iurl] (internal link); there is no [span style=…] here (that exists only in actor/reaction BBCode). Mapping: [color]→<span style="color">, [bg]→<span style="background-color">, [div]→<div>.

Verified support matrix (built by rendering a live probe string, not read off the swagger):

Renders BBCode ✅Stays literal ❌
label.valueedit value / placeholder / multiline
button.titleselect value · multiselect option
check.titleradio option title
stepper option titletoggle.title · tab option title
form.title, mainMenu title, comments, carousel item title, upload.titlecopy.title
table head title AND plain cell

Heuristic: it expands where the text lands in a plain text node (<span>/<div>/<td>) and stays literal where it lands in a form-control value/placeholder — but it's only a rough heuristic: it's whether that specific component passes its title through bbCodeToHtml/dangerouslySetInnerHTML, not the node type. check ✅ but radio ❌ even though both render a <label>; stepper ✅ but toggle/tab ❌. Trust the matrix (built from the renderer source), not the swagger: the swagger marks only label/button/mainMenu as bbcode fields, yet table cells clearly render it. When a field isn't in the matrix, confirm it in the live UI before relying on it.

Styling use — inject a styleable element where there is none. A plain table cell is bare text in a <td> (no inner wrapper to hook). [div]…[/div] gives you an inner <div> to style as a pill/badge (.<cellClass> div { … }); [bg]/[color] give inline <span>s. (Alternative with no inner element: absolutely-position the <td> so it shrink-wraps its text into a pill.)


Workflow (reuses the Smart Form tools)

1. pullSmartForm(actorId)        → downloads <actorId>/develop/ + production/ (incl. style / styles/, pages/<id>/style)
2. Edit ONLY under develop/:
     - styles/index (+ partials)  OR  the root `style` file
     - pages/<id>/style           (page-specific)
     - pages/<id>/config          (only to add/adjust a styleClass hook)
3. pushSmartForm(actorId)        → validates + uploads changed files; style/styles files carry MIME text/css (the push tool sets it; the server stores the client-supplied type — it does not force it by folder)
4. deploySmartForm(actorId)      → publishes develop → production (when approved)

Rules: edit develop only (production is readonly); pullSmartForm first (it writes the .manifest.json push needs). Files under styles/ are sent as text/css; everything else application/json.

Verifying a styling result. appGetPage returns the server-resolved config (locale/viewModel expanded) — it does not run CSS compilation or bbCodeToHtml, so you cannot confirm a visual result or whether BBCode expanded from it. Visual/BBCode results are verified in the live UI by the user.

But you CAN and SHOULD confirm the Less compiles — locally, before pushing. This is the one styling failure that is otherwise completely silent: a compile error is not raised at serve time, it is emitted as a /* Less Error … */ comment and the page renders unstyled. pushSmartForm does not compile CSS and appGetPage never returns it, so nothing in the toolchain reports it.

Two details make the naive command fail, so use this recipe as-is (verified against a real 6-file app):

  • Smart Form partials are stored without a .less extension, and Less resolves @import "colors_fonts" as colors_fonts.less — so copy them to *.less first.
  • lessc cannot read a process-substitution path (<(…) → EBADF: bad file descriptor), so write a real temp file.
  • The npm package is less (the binary is lessc); a package literally named lessc also exists on npm and is not the compiler — always pass --package=less.
bash
cd <actorId>/develop
rm -rf .lesscheck && mkdir .lesscheck
for f in styles/*; do cp "$f" ".lesscheck/$(basename "$f").less"; done

# the main sheet, wrapped exactly as the server wraps it (this is what makes `&` the page root)
{ echo '.cdu-page {'; sed 's/@import "\(.*\)";/@import "\1.less";/' styles/index; echo '}'; } \
  > .lesscheck/main.less
npx --yes --package=less lessc .lesscheck/main.less /dev/null && echo "OK styles/index"

# each page sheet, with the tokens prepended so its variables resolve
for p in pages/*/style; do
  { echo '.cdu-page {'; echo '@import "colors_fonts.less";'; echo '@import "init_styles.less";';
    cat "$p"; echo '}'; } > .lesscheck/page.less
  npx --yes --package=less lessc .lesscheck/page.less /dev/null && echo "OK $p"
done
rm -rf .lesscheck

Verified while doing this: @font-face and @media blocks correctly bubble out of the .cdu-page { … } wrapper, so keeping them in an imported partial is safe.

When to ask for the rendered HTML. First fix from the DOM map below; if it doesn't land, correct once more. If after the second correction the user still doesn't get the expected result, stop guessing and ask them to copy the element's HTML (Inspect → Copy outerHTML of the element, or dump #page). The real DOM — hashed classes, wrapper nesting, inline sizes — resolves what the map can't; most restyle stalls end the moment you read it.


Starter kit — snippets that recur across real forms

These appear near-verbatim in multiple production forms; lift them as a baseline.

colors_fonts — tokens + font (define here; everything, incl. page styles, inherits them).

less
// color tokens (flat is fine for small/medium forms; mature toward Less maps for large apps)
@color_primary:#151f6d; @color_white:#fff; @color_black:#0f0f0f; @color_border:#EAECF0;
@color_grey:#b0adb7; @font_main:'Inter';
// status palette as one token per state (drives chips/rows by status)
@color_new:#A7D8F0; @color_in_progress:#A7E3B1; @color_completed:#B6E3C5; @color_pending:#FBE7A1;
// icons/logos as URL tokens (proxied workspace assets or relative attachments/…)
@icon_search: url('https://…/api/1.0/download/…svg?preview=true');
@font-face { font-family:'Inter'; font-weight:100 900; font-display:swap;
  src:url(https://fonts.gstatic.com/…woff2) format('woff2'); unicode-range:U+0000-00FF, …; }

init_styles — platform resets + project-wide component defaults. @imported from styles/index after colors_fonts. Page-specific exceptions do not go here (→ pages/<id>/style).

less
// ── 1. core resets ───────────────────────────────────────────────────────────
*, *::before, *::after { box-sizing: border-box; }            // the single key reset
// Every .section__content ships its OWN grey bg + padding:20px 16px 0 + margin-bottom:20px
// (some rules theme-scoped → 0,2,0), so it paints a grey padded box INSIDE your cards.
// Neutralize it once here; re-add padding on your own card wrapper. !important beats the theme rule.
.section .section__content { background:transparent !important; margin-bottom:0 !important; padding:0 !important; }
.label, .button, .form { margin:0 !important; font-size:16px; line-height:1.5; }
[class*="row__item"] { display: contents; }                   // row/w items flow into the parent
.button-wrapper { width:auto !important; display:block; }
.button-wrapper:has(.hidden) { display:none; }

// ── 2. platform chrome (header/footer) — NOT hidden by default; reachable via:
//   & { #pageWrap > [class*="content"] > [class*="header"],
//       #pageWrap > [class*="content"] > [class*="footer"] { display:none !important; } }

// ── 3. (optional) full-bleed — drop the default centered/capped card ─────────
.content__main { padding:0 !important;
  [data-class="grid"] { display:block; }
  [data-class="grid-one-column"] { max-width:none; } }

// ── 4. project font (token from colors_fonts) ────────────────────────────────
input, textarea, .button > span, .toggle__title,
:not(&) body > .popoverContent, span[class*="label"], & { font-family:@font_main, sans-serif; }

// ── 5. utilities ──────────────────────────────────────────────────────────────
.visually_hidden { position:absolute; width:1px; height:1px; margin:-1px; padding:0; border:0;
  white-space:nowrap; clip-path:inset(100%); clip:rect(0 0 0 0); overflow:hidden; }
.font_12{font-size:12px;} .font_14{font-size:14px;} .mb_10{margin-bottom:10px !important;}

// ── 6. loading overlays (platform spinners) ──────────────────────────────────
[class*="button__spinner__wrap"], [class^="table__spinner__wrap"] {
  position:fixed; inset:0; z-index:10000; display:flex; align-items:center;
  justify-content:center; background:rgba(0,0,0,.1); backdrop-filter:blur(1px); }

// ── 7. project-wide component defaults (customize to brand) ──────────────────
.button {                                   // ONE base button look used on every page
  padding:14px 20px; border:none; border-radius:10px; font-size:15px; font-weight:600;
  cursor:pointer; line-height:1; display:flex; align-items:center; justify-content:center; gap:8px;
  transition:all .3s cubic-bezier(.4,0,.2,1);
  &.primary_btn { background:@color_primary; color:@color_white;
    &:hover { transform:scale(1.02); box-shadow:0 6px 20px fade(@color_primary,40%); } }
  // … add your semantic variants (.submit_btn, .cancel_btn, …) by this pattern …
}
.modal {                                    // default sizing by modalSize
  &[class*="small"]  { width:400px !important; }
  &[class*="medium"], &[class*="large"] { width:auto !important; max-height:90% !important; }
  &[class*="xlarge"] { width:80% !important; max-height:90% !important; }
}

Pattern catalogue (reusable recipes)

Layout

  • Centered card form — cap and center the grid wrapper: [data-class="grid-one-column"] { max-width:440px; margin:20px auto; padding:20px; background:#fff; border-radius:8px; box-shadow:0 0 10px #0000001a; }
  • Full-bleed admin — unlock the cap: .content__main [data-class="grid-one-column"] { max-width:none; }
  • Fixed sidebar shell — .sidebar { width:250px; grid-template-rows:80px 1fr 120px; } with header/content/footer panes.
  • Sidebar styleClass + mainMenu indent trap — the sidebar rail ignores grid.sideBar.styleClass; style it via the base slots .sidebar / .sidebar__header|__content|__footer / .page__sidebar (layout — fixed width, pinned footer — as in Fixed sidebar shell above). For a mainMenu inside it, do not target [class*="mainMenuItem"] — the substring also matches the branch wrappers (.mainMenuItemGroup, __summary, __dropdown), so padding accumulates per level into huge gaps. Zero the wrappers, style only the exact .mainMenuItem (the clickable row), and indent by level with the renderer's --depth var:
    less
    nav.mainMenu [class*="mainMenuItemGroup"], nav.mainMenu details, nav.mainMenu summary { margin:0 !important; padding:0 !important; }
    nav.mainMenu .mainMenuItem { padding:7px 10px; padding-left:calc(10px + var(--depth,0) * 14px); }
  • Pinned footer + scrolling body — .info_form{height:calc(100vh - 50px); display:flex; flex-direction:column;} .content_section{flex:1; max-height:calc(100vh - 100px);}
  • Card-per-iteration wrapper — a bare contentLoop renders its iterations flat, with no per-iteration box. Set the section's sortable:true and the renderer wraps each iteration in a .draggable card with a drag handle; hide the handle to drop the drag affordance and keep just the wrapper (hiding .draggable__handle fully disables dragging — it is the only activator — so there is no reorder submit to handle):
    less
    .section__content .draggable__handle { display:none !important; }          /* kill grip → drag off */
    .section__content .draggable { border:none !important; background:transparent !important;
      padding:0 !important; box-shadow:none !important; cursor:default; }       /* reset outer card, avoid double frame */
    .section__content .draggable__content { border:1px solid #ececec; border-radius:10px; padding:14px; }
  • Give a row its own class (multi-token row) — styleClass on a row is dropped, but the row value is space-separated: the first token is the row id (→ .row__<id>), and every extra token the renderer adds as a literal class on the row wrapper. So row:"1 my_row" and row:"2 my_row" both get .my_row — a shared, stable hook to style many rows at once (the hashed .row__… is per-build and unreliable).

Components — re-skin, don't accept defaults

  • Floating-label input — .edit__text .label { top:36px; } .edit__text.selected .label { top:16px; font-size:12px; } .edit__text.focus .field { border-color:@blue; box-shadow:0 0 0 2px #007bff40; }
  • Custom checkbox — hide native (input{-webkit-appearance:none}, i{display:none}), draw tick on .checked input::after (rotated border).
  • Card data table — .main_table { border-radius:12px; border:1px solid @border; } .main_table [class*="table__wrap"]{ overflow:hidden; border-radius:12px 12px 0 0; } + custom pagination by swapping [id*="table-next"] i::after { content:""; background-image:@icon_arrow_right; }. Use modifier classes: .main_table.thin_table, .no_thead, .center_cell.
  • Table/radio → cards (the e-commerce look): thead{display:none}, tbody{display:flex;flex-direction:column;gap:10px}, .table-row{border:1px solid #ddd;border-radius:8px;padding:10px}, highlight selection with .table-row:has([class*="radioItem"].checked){border-color:#34303d}.
  • Table pagination at the bottom, centered — a table type:"group"'s pagination is a sibling of [class*="table__wrap"] (not part of the rows) and defaults to position:absolute (pinned top, overlapping content). Lay the table root as a column and re-place it:
    less
    .myTable { display:flex; flex-direction:column; }
    .myTable [class*="table__wrap"] { max-height:none !important; height:auto !important; }
    .myTable [class*="table__pagination"] { position:static !important; order:99;
      display:flex; justify-content:center; gap:8px; margin-top:20px; }
  • Status chip — state-class + token map: .status_cell.completed-… div{ background:@color_completed; } (backend sets styleClass:"status_cell completed-…").
  • Button variants — base .button + semantic modifiers (.blue_btn, .red_button, .submit_btn); hover-lift (transform:scale(1.02); box-shadow:…) or an animated ::after fill-sweep (transform:skewX(-20deg)).
  • Toast notifications — centered, severity icon via [class*="notifyItem"][class*="success"]::before{ background-image:@icon_success; }.
  • Modal sizing — base .modal + :has(.<innerSection>) to size each, or size-class match &[class*="xlarge"]{ width:80% !important; }.
  • Breadcrumbs (via tab) — model a crumb trail as a tab (options[] = crumbs, each {value,title}; value = current; submitOnChange:true so a click posts the value). Two tab traps surface only once you restyle it: the label [class*="i-label"] defaults to width:0; overflow:hidden (text clips to nothing), and in a fixed tab (i-fixed) each [class*="tab__item"] collapses to width:0 (options overlap). Fix both with width:auto:
    less
    .breadcrumbs [class*="tab__item"] { width:auto !important; min-width:max-content !important;
      flex:0 0 auto !important; margin:0 !important; }
    .breadcrumbs [class*="tab__item"] [class*="i-label"] { width:auto !important; overflow:visible !important; }
    .breadcrumbs [class*="tab__item"]:not(:last-child)::after { content:"›"; margin:0 10px; opacity:.6; }

Interactivity (CSS-only, no backend round-trip)

  • Accordion / dropdown / collapsible tree — a check/toggle with .checked, expanded by parent :has(), animated via grid-template-rows:0fr → 1fr:
    less
    .menu { display:grid; grid-template-rows:0fr; overflow:hidden; transition:grid-template-rows .32s; }
    .sidebar:has(.nav_btn.checked) .menu { grid-template-rows:1fr; }
    When the toggle and its target are siblings in the same .section__content (the common case), prefer the sibling combinator — no :has() needed: .myCheck.checked ~ .myCollapsible { max-height:300px; }. .checked lands on the component's outer wrapper. Reserve :has() for non-sibling targets.
  • Slide-in panel / popover — toggle opacity/transform/pointer-events under &:has(.trigger.checked).

State without CSS-only tricks

  • Screens-as-sections — put each step/success/error/loading state as a sibling section, the backend flips visibility (one stylesheet, many screens).
  • Hidden state carriers — edit items with styleClass:"visually_hidden" stash client state (selected id, cart id) for submit.

Icons

  • Recolorable monochrome — mask:url(@icon) no-repeat center; mask-size:contain; background-color:currentColor; (inherits text color). Bundled asset: relative attachments/cart.svg; external: proxied /api/1.0/download|image.
  • Full-color — background-image:@icon token.

RTL / theme toggle from the backend

  • Page root styleClass:"{{settings_page_text_align}}"; rule .form.text_align_right { … flex-direction:row-reverse; text-align:right; … }. The backend pushes one value to mirror the whole UI. Same mechanism toggles nav-active state, light/dark, etc.

Design-system approach (advanced — for large/branded apps)

When a form grows or needs strict brand consistency, build a token system in Less (this is the LMS form's approach — Tailwind/Untitled-UI in Less):

less
// maps for everything, not just color
@breakpoint: { minimum-mobile:375px; mobile-tablet:820px; tablet-desktop:1180px; }
@space:      { 0:2px; 1:4px; 2:8px; 3:12px; 4:16px; 5:20px; 6:24px; 8:32px; /* … */ 64:256px; }
@color: { @gray:{25:#FCFCFD; /* … */ 700:#344054; 900:#101828;} @brand:{…} @error:{…} @success:{…} }

// generate utility classes from a map (each + guard)
.make-space-set(@base,@rule,@min,@max){ each(@space,{ .apply(@k,@v);
  .apply(@k,@v) when (@k<=@max) and (@k>=@min){ .@{base}-@{k}.@{base}-@{k}{ @{rule}:@v; } } }); }
.make-space-set(mt, margin-top, 0, 16);   // → .mt-0 … .mt-16, applied in styleClass

// typography scale via lookup mixin
.text-font(@size; @weight:regular){ @s:@typography[@text][@@size];
  font-size:@s[font-size]; font-weight:@font-weight[@@weight]; line-height:@s[line-height]; }

// responsive tokens: Less map → CSS var overridden per breakpoint
@media (min-width:@breakpoint[tablet-desktop]) { :root { --space-large: 32px; } }
.gap-large { gap: var(--space-large); }

Advanced Less available: each(), guards (when), maps + indirect lookup (@@key), replace(), extract()/if()/isnumber()/isdefined(), detached-ruleset returns (mixin()[@result]). Caveat: keep this in a modular styles/ split (colors_fonts, space, typography, shadows, mixins, index) — a single 7k-line file is the maintainability ceiling.


Gotchas (learned from real forms)

  • Windows filenames: a folder/file titled with a reserved char (\ / : * ? " < > |) breaks pullSmartForm on Windows (mkdir fails). Don't name definitions like edit|create_…. If a pull aborts on mkdir … syntax is incorrect, rename the offending folder in the editor and re-pull (or recover content from a .graph export that carries scripts/<uuid>/… file trees).
  • Winning the cascade: component CSS-modules load after you and win equal-specificity ties — beat them with a chain of stable classes (styleClass + [class*="…"] + tag), not #id or a lone !important. Full worked example + cascade math in §"How Smart Form styling works" pt 5.
  • Never write .cdu-page yourself: the compiler wraps your whole stylesheet in .cdu-page { … } at serve-time, so a hand-written .cdu-page (or .cdu-page:has(…)) becomes a descendant (.cdu-page .cdu-page…) and silently never matches. The page root is & (= .cdu-page): paint the page with & { background:#e30613; min-height:100vh; } and target it with &:has(…).
  • Image/file cell renders "wide but a thin strip": not a width bug but a clipping ancestor — a wrapper (.file / .file__item) keeps a small fixed height + overflow:hidden. Force the box at every wrapper level (height + min/max-height + width:100% + max-width:none + overflow), not only on <img>. The default clamp is max-height:32px on both <img> and [class*="table__img"], and max-height always beats height — so max-height:none is the key. One height knob down the whole chain (.card-img = the cell's styleClass, applied on <td>):
    less
    .card-img, .card-img [class*="table__img"], .card-img .file, .card-img [class*="file__item"] {
      width:100%; height:240px !important; max-height:none !important; display:block; }
    .card-img img { width:100% !important; height:100% !important; max-width:none !important;
      max-height:none !important; object-fit:contain; }   /* contain = whole image; cover = fill + crop */
  • Dead styleClass: placeholder classes (my-custom-class) with no rule are harmless but noise — remove them.
  • Stale copies: forms accumulate style(copy) / style BACKUP files — they're not wired in; don't analyze or ship them.
  • External images are proxied: https://… URLs in CSS get /api/1.0/image?src= prepended automatically (actor-attached images are internal and not proxied).
  • // comments compile away (prefer over /* */); keep nesting < 3 levels.
  • Edit develop only; pages/<id>/style needs no @import (auto-included).

Reference documents

PathWhen to read
$CLAUDE_PLUGIN_ROOT/docs/user-flows/cdu-dom-tree-reference.mdThe rendered-DOM map: full per-component tag tree + class names, stable hooks vs. volatile hashes, page skeleton & theme classes, all table types, and which components need backend data to render
$CLAUDE_PLUGIN_ROOT/docs/user-flows/cdu-page-protocol.mdComponent class catalogue, styleClass, templating, change protocol
$CLAUDE_PLUGIN_ROOT/docs/user-flows/smart-forms.mdProject file structure, deploy/release, styles compilation
$CLAUDE_PLUGIN_ROOT/skills/simulator-smart-forms/SKILL.mdThe pull/push/deploy cycle and page-config format this skill builds on
CDU swagger → "CSS styling" tagAuthoritative Less guide: organization, component default classes, best practices, image proxying

Frequently asked questions

What does the Simulator Styles AI skill do?

Simulator.Company Smart Form (CDU) STYLING specialist — authoring complex Less/CSS for Smart Forms: theme tokens, page/form/section layout, component re-skinning, reusable style patterns, responsive and design-system approaches. Use when the user wants to STYLE or RESTYLE an existing Smart Form / CDU app — change its look, build a theme, style a table / sidebar / modal / form, add a design system, fix spacing/colors/fonts, or apply a complex visual design. This skill owns the `style` / `styles/` layer; it reuses the Smart Form tools (pullSmartForm / pushSmartForm / deploySmartForm) but does...

Why use Simulator Styles on TypingMind?

Because you install it once and use it with any model. Simulator Styles is plain Markdown rather than provider-specific code, so the same skill runs on GPT-5, Claude, Gemini, Grok, or a local model — and you can switch model mid-chat without it breaking. TypingMind runs on your own API keys, so you pay providers directly instead of a per-seat subscription, and your skills and chats stay in your own storage.

How do I install Simulator Styles in TypingMind?

Open Plugins → Skills → Install from GitHub in TypingMind and paste https://github.com/corezoid/simulator-ai-plugin/tree/main/plugins/simulator/skills/simulator-styles. TypingMind reads its SKILL.md and installs it as a skill you can enable per chat.

Which AI models can use Simulator Styles?

Any model you connect in TypingMind. AI skills are plain Markdown instructions rather than provider-specific code, so GPT, Claude, Gemini, Grok, and local models can all load this skill when a request matches it.

How many AI models can I use with Simulator Styles?

As many as you like. As long as a model supports skills, you can use Simulator Styles with it — GPT, Claude, Gemini, Grok, DeepSeek, Mistral, Llama and more — all on TypingMind with your own API keys.

Is the Simulator Styles AI skill free?

Yes. It is published on GitHub by corezoid under the MIT license. You only pay your own AI provider for the tokens you use.

What are AI skills?

An AI skill is a reusable instruction bundle that teaches an AI model how to do one specific task. It follows the open Agent Skills format: a SKILL.md file with a name and description, plus any scripts, templates or reference files the model may need. The model reads the instructions only when your request matches the skill, so an installed skill costs nothing until it is used.

How are AI skills different from plugins or MCP servers?

A plugin or MCP server gives a model new tools to call — code that runs somewhere and returns a result. An AI skill gives the model knowledge and process instead: how to approach a task, which steps to follow, what good output looks like. Skills are plain Markdown, so they need no server, no API key and no runtime, and they work with any model.

View all

Set up your own AI workspace now

Get notified about new features and future giveaways by subscribing to our newsletter 👇