[{"data":1,"prerenderedAt":3193},["ShallowReactive",2],{"post-015-design-system-invariants-ru":3,"post-og-015-design-system-invariants":1722},{"id":4,"title":5,"bars":6,"blurb":7,"body":8,"color":6,"contacts":6,"date":1708,"dateModified":6,"description":1709,"extension":1710,"featured":1231,"groups":6,"kicker":1711,"meta":1712,"metrics":6,"n":1713,"navigation":1231,"openTabs":6,"path":1714,"progress":6,"readTime":1715,"reading":6,"role":6,"rules":6,"running":6,"seo":1716,"ships":6,"slug":1717,"stack":6,"started":6,"status":6,"stem":1718,"streak":6,"tag":1719,"tagColor":1720,"tagline":6,"tasks":6,"timeline":6,"topics":6,"week":6,"year":6,"__hash__":1721},"ru\u002Fwriting\u002F015-design-system-invariants.md","Дизайн-система как набор инвариантов: собираем токен-пайплайн с нуля",null,"часть 1 серии про claude-driven монорепу — дизайн-слой, чьи правила делают неправильное невозможным, а не «не рекомендованным».",{"type":9,"value":10,"toc":1694},"minimark",[11,15,19,24,27,50,55,58,61,73,76,80,87,98,101,105,126,272,275,311,314,345,369,373,384,387,720,727,825,828,831,1001,1008,1029,1046,1060,1080,1084,1095,1098,1104,1111,1290,1297,1308,1315,1327,1331,1337,1343,1440,1450,1456,1481,1488,1502,1508,1520,1524,1527,1559,1566,1570,1573,1609,1613,1624,1663,1666,1673,1685,1690],[12,13,14],"p",{},"▸ избранное · лонгрид — № 015 · 13 июн '26 · 12 мин · серия: claude-driven монорепа · ч.1",[16,17,5],"h2",{"id":18},"дизайн-система-как-набор-инвариантов-собираем-токен-пайплайн-с-нуля",[12,20,21],{},[22,23,7],"em",{},[25,26],"hr",{},[28,29,30],"blockquote",{},[12,31,32,33,40,41,45,46,49],{},"это первая статья серии о принципах, на которых стоит ",[34,35,39],"a",{"href":36,"rel":37},"https:\u002F\u002Fgithub.com\u002Fevgentus-cy\u002Fclaude-driven-nest-nuxt-flutter-monorepo",[38],"nofollow","claude-driven монорепа",".\nрепозиторий описывает себя как ",[42,43,44],"strong",{},"«spec-first, agent-friendly monorepo boilerplate»"," — стартер под продакшен для команд, которые строят продукт в связке ",[42,47,48],{},"Claude Design → Claude Code",": рабочие backend \u002F web \u002F mobile, общий пайплайн дизайн-токенов, единый источник правды для каждого контракта и защита от дрейфа, которая делает агентную разработку безопасной «без человека над каждой строкой». сегодня — дизайн-слой, подробно и по шагам, так, что ты сможешь собрать такой же пайплайн у себя. стек репы (NestJS \u002F Nuxt \u002F Flutter) — лишь рабочий пример: подход переносится на любой.",[51,52,54],"h3",{"id":53},"зачем-вообще-инварианты","зачем вообще инварианты",[12,56,57],{},"у greenfield-проектов с AI-ассистентом провалы предсказуемы: агент зашивает цвет хардкодом прямо в компонент, переименовывает поле в одном клиенте и забывает в другом, меняет что-то «по месту». к моменту ревью диф уже на десятки файлов, и человек ловит дрейф слишком поздно.",[12,59,60],{},"вывод, на котором стоит весь подход:",[28,62,63],{},[12,64,65,68,69,72],{},[42,66,67],{},"дизайн-система — это не библиотека компонентов, а набор машинно-проверяемых инвариантов.","\nты делаешь неправильное ",[22,70,71],{},"невозможным",", а не «не рекомендованным». тогда неважно, кто пишет код — человек или агент: правила одинаково держат и тех, и других.",[12,74,75],{},"ниже — как собрать такую систему по шагам.",[51,77,79],{"id":78},"схема-целиком","схема целиком",[12,81,82,83,86],{},"один нейтральный формат в центре, и к нему сходятся ",[42,84,85],{},"и входы, и выходы",":",[88,89,94],"pre",{"className":90,"code":92,"language":93},[91],"language-text","  источники дизайна                                  таргеты стека\n  ─────────────────                                  ────────────\n  Claude Design  ┐                              ┌─►  CSS \u002F SCSS-переменные\n  Figma          ├─►  [ ТОКЕНЫ: W3C DTCG ]  ──► эмиттеры ─┼─►  TypeScript-константы\n  Penpot         ├─►   единый источник       (по таргету)  ├─►  Flutter\u002FDart-константы\n  вручную (JSON) ┘                              └─►  …любой другой\n                          │\n                          ▼\n                  библиотека примитивов на токенах  ──►  экраны продукта\n                          │\n                          ▼\n                  машинные правила (линтеры · аудит · CI-drift)\n","text",[95,96,92],"code",{"__ignoreMap":97},"",[12,99,100],{},"дальше — каждый блок по шагам.",[51,102,104],{"id":103},"шаг-1-источник-токены-в-нейтральном-формате","шаг 1 · источник: токены в нейтральном формате",[12,106,107,108,111,112,117,118,121,122,125],{},"источник правды — ",[42,109,110],{},"семантические токены"," в формате ",[34,113,116],{"href":114,"rel":115},"https:\u002F\u002Fwww.designtokens.org\u002F",[38],"W3C Design Tokens (DTCG)",". формат намеренно не знает ни одного фреймворка: лист — это пара ",[95,119,120],{},"$value"," \u002F ",[95,123,124],{},"$type",", плюс пары тем.",[88,127,131],{"className":128,"code":129,"language":130,"meta":97,"style":97},"language-json shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","\u002F\u002F specs\u002Fdesign\u002Ftokens\u002Fcolor.json\n\"accent\": {\n  \"light\": { \"$value\": \"#635bff\", \"$type\": \"color\" },\n  \"dark\":  { \"$value\": \"#7b75ff\", \"$type\": \"color\" }\n}\n","json",[95,132,133,142,162,217,266],{"__ignoreMap":97},[134,135,138],"span",{"class":136,"line":137},"line",1,[134,139,141],{"class":140},"sHwdD","\u002F\u002F specs\u002Fdesign\u002Ftokens\u002Fcolor.json\n",[134,143,145,149,153,155,159],{"class":136,"line":144},2,[134,146,148],{"class":147},"sMK4o","\"",[134,150,152],{"class":151},"sfazB","accent",[134,154,148],{"class":147},[134,156,158],{"class":157},"sTEyZ",": ",[134,160,161],{"class":147},"{\n",[134,163,165,168,172,174,176,179,182,185,187,189,191,194,196,199,201,203,205,207,209,212,214],{"class":136,"line":164},3,[134,166,167],{"class":147},"  \"",[134,169,171],{"class":170},"spNyl","light",[134,173,148],{"class":147},[134,175,86],{"class":147},[134,177,178],{"class":147}," {",[134,180,181],{"class":147}," \"",[134,183,120],{"class":184},"sBMFI",[134,186,148],{"class":147},[134,188,86],{"class":147},[134,190,181],{"class":147},[134,192,193],{"class":151},"#635bff",[134,195,148],{"class":147},[134,197,198],{"class":147},",",[134,200,181],{"class":147},[134,202,124],{"class":184},[134,204,148],{"class":147},[134,206,86],{"class":147},[134,208,181],{"class":147},[134,210,211],{"class":151},"color",[134,213,148],{"class":147},[134,215,216],{"class":147}," },\n",[134,218,220,222,225,227,229,232,234,236,238,240,242,245,247,249,251,253,255,257,259,261,263],{"class":136,"line":219},4,[134,221,167],{"class":147},[134,223,224],{"class":170},"dark",[134,226,148],{"class":147},[134,228,86],{"class":147},[134,230,231],{"class":147},"  {",[134,233,181],{"class":147},[134,235,120],{"class":184},[134,237,148],{"class":147},[134,239,86],{"class":147},[134,241,181],{"class":147},[134,243,244],{"class":151},"#7b75ff",[134,246,148],{"class":147},[134,248,198],{"class":147},[134,250,181],{"class":147},[134,252,124],{"class":184},[134,254,148],{"class":147},[134,256,86],{"class":147},[134,258,181],{"class":147},[134,260,211],{"class":151},[134,262,148],{"class":147},[134,264,265],{"class":147}," }\n",[134,267,269],{"class":136,"line":268},5,[134,270,271],{"class":147},"}\n",[12,273,274],{},"два принципа закладываются прямо здесь:",[276,277,278,285],"ol",{},[279,280,281,284],"li",{},[42,282,283],{},"единый источник правды."," все дизайн-решения (цвет, типографика, отступы, радиусы, тени, движение, z-index) живут в одном месте — в токен-файлах. всё остальное либо генерируется из них, либо ссылается.",[279,286,287,290,291,293,294,293,297,300,301,121,303,306,307,310],{},[42,288,289],{},"имя = намерение, не значение."," токен называется ",[95,292,152],{},", ",[95,295,296],{},"surface.raised",[95,298,299],{},"radius.xl"," — по роли, а не ",[95,302,193],{},[95,304,305],{},"16px",". и вот в чём сила: ребрендинг становится правкой одной строки. в этой репе так и было — акцент мигрировал ",[95,308,309],{},"#5c16c5 → #635bff",", и ни один компонент не поменялся, потому что компоненты ссылаются на роль, а не на хекс.",[12,312,313],{},"откуда брать сам источник — выбираешь ты, и вариантов много (всё, что умеет отдавать DTCG):",[315,316,317,323,333,339],"ul",{},[279,318,319,322],{},[42,320,321],{},"Claude Design \u002F claude.ai"," — описываешь бренд брифом, Claude генерит токен-JSON и мокапы. в репе для этого лежит готовый copy-paste бриф. это «AI как источник дизайна».",[279,324,325,328,329,332],{},[42,326,327],{},"Figma"," — экспорт токен-плагина в JSON, раскладка по ",[95,330,331],{},"*.json",".",[279,334,335,338],{},[42,336,337],{},"Penpot"," (опенсорсная альтернатива Figma) — тот же экспорт плагина.",[279,340,341,344],{},[42,342,343],{},"вручную"," — просто пишешь JSON.",[28,346,347],{},[12,348,349,350,357,358,361,362,332],{},"→ исходники и формат: ",[34,351,354],{"href":352,"rel":353},"https:\u002F\u002Fgithub.com\u002Fevgentus-cy\u002Fclaude-driven-nest-nuxt-flutter-monorepo\u002Ftree\u002Fmain\u002Fspecs\u002Fdesign",[38],[95,355,356],{},"specs\u002Fdesign"," (в её ",[95,359,360],{},"README.md"," описан импорт из Claude Design \u002F Figma \u002F Penpot \u002F вручную) · и ",[34,363,366],{"href":364,"rel":365},"https:\u002F\u002Fgithub.com\u002Fevgentus-cy\u002Fclaude-driven-nest-nuxt-flutter-monorepo\u002Ftree\u002Fmain\u002Fspecs\u002Fdesign\u002Ftokens",[38],[95,367,368],{},"specs\u002Fdesign\u002Ftokens",[51,370,372],{"id":371},"шаг-2-эмиттер-один-источник-много-таргетов","шаг 2 · эмиттер: один источник → много таргетов",[12,374,375,376,379,380,383],{},"это сердце переносимости. берём одну токен-модель и ",[42,377,378],{},"компилируем"," под каждую платформу в её ",[22,381,382],{},"нативный"," формат. никаких ручных «продублируй цвет в трёх местах» — паритет структурный.",[12,385,386],{},"эмиттер для CSS проходит по токенам и раскладывает их по темам (имена camelCase → kebab-case):",[88,388,392],{"className":389,"code":390,"language":391,"meta":97,"style":97},"language-ts shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","\u002F\u002F упрощённо из packages\u002Fdesign-tokens\u002Fsrc\u002Femit-scss.ts\nfunction themedBlock(theme, tokens) {\n  const selector = theme === 'light' ? `:root, [data-theme='light']` : `[data-theme='${theme}']`;\n  const lines = [`${selector} {`];\n  for (const [key, leaf] of Object.entries(tokens.color.brand)) {\n    const value = (leaf[theme] ?? leaf.light).$value; \u002F\u002F fallback на light\n    lines.push(`  --brand-${kebab(key)}: ${value};`);\n  }\n  lines.push('}');\n  return lines.join('\\n');\n}\n","ts",[95,393,394,399,426,487,517,571,616,661,667,689,715],{"__ignoreMap":97},[134,395,396],{"class":136,"line":137},[134,397,398],{"class":140},"\u002F\u002F упрощённо из packages\u002Fdesign-tokens\u002Fsrc\u002Femit-scss.ts\n",[134,400,401,404,408,411,415,417,420,423],{"class":136,"line":144},[134,402,403],{"class":170},"function",[134,405,407],{"class":406},"s2Zo4"," themedBlock",[134,409,410],{"class":147},"(",[134,412,414],{"class":413},"sHdIc","theme",[134,416,198],{"class":147},[134,418,419],{"class":413}," tokens",[134,421,422],{"class":147},")",[134,424,425],{"class":147}," {\n",[134,427,428,431,434,437,440,443,446,448,451,454,457,460,463,466,468,471,474,476,479,482,484],{"class":136,"line":164},[134,429,430],{"class":170},"  const",[134,432,433],{"class":157}," selector",[134,435,436],{"class":147}," =",[134,438,439],{"class":157}," theme",[134,441,442],{"class":147}," ===",[134,444,445],{"class":147}," '",[134,447,171],{"class":151},[134,449,450],{"class":147},"'",[134,452,453],{"class":147}," ?",[134,455,456],{"class":147}," `",[134,458,459],{"class":151},":root, [data-theme='light']",[134,461,462],{"class":147},"`",[134,464,465],{"class":147}," :",[134,467,456],{"class":147},[134,469,470],{"class":151},"[data-theme='",[134,472,473],{"class":147},"${",[134,475,414],{"class":157},[134,477,478],{"class":147},"}",[134,480,481],{"class":151},"']",[134,483,462],{"class":147},[134,485,486],{"class":147},";\n",[134,488,489,491,494,496,500,503,506,508,510,512,515],{"class":136,"line":219},[134,490,430],{"class":170},[134,492,493],{"class":157}," lines",[134,495,436],{"class":147},[134,497,499],{"class":498},"swJcz"," [",[134,501,502],{"class":147},"`${",[134,504,505],{"class":157},"selector",[134,507,478],{"class":147},[134,509,178],{"class":151},[134,511,462],{"class":147},[134,513,514],{"class":498},"]",[134,516,486],{"class":147},[134,518,519,523,526,529,531,534,536,539,541,544,547,549,552,554,557,559,561,563,566,569],{"class":136,"line":268},[134,520,522],{"class":521},"s7zQu","  for",[134,524,525],{"class":498}," (",[134,527,528],{"class":170},"const",[134,530,499],{"class":147},[134,532,533],{"class":157},"key",[134,535,198],{"class":147},[134,537,538],{"class":157}," leaf",[134,540,514],{"class":147},[134,542,543],{"class":147}," of",[134,545,546],{"class":157}," Object",[134,548,332],{"class":147},[134,550,551],{"class":406},"entries",[134,553,410],{"class":498},[134,555,556],{"class":157},"tokens",[134,558,332],{"class":147},[134,560,211],{"class":157},[134,562,332],{"class":147},[134,564,565],{"class":157},"brand",[134,567,568],{"class":498},")) ",[134,570,161],{"class":147},[134,572,574,577,580,582,584,587,590,592,595,598,600,602,604,606,608,610,613],{"class":136,"line":573},6,[134,575,576],{"class":170},"    const",[134,578,579],{"class":157}," value",[134,581,436],{"class":147},[134,583,525],{"class":498},[134,585,586],{"class":157},"leaf",[134,588,589],{"class":498},"[",[134,591,414],{"class":157},[134,593,594],{"class":498},"] ",[134,596,597],{"class":147},"??",[134,599,538],{"class":157},[134,601,332],{"class":147},[134,603,171],{"class":157},[134,605,422],{"class":498},[134,607,332],{"class":147},[134,609,120],{"class":157},[134,611,612],{"class":147},";",[134,614,615],{"class":140}," \u002F\u002F fallback на light\n",[134,617,619,622,624,627,629,631,634,636,639,642,644,646,648,651,653,655,657,659],{"class":136,"line":618},7,[134,620,621],{"class":157},"    lines",[134,623,332],{"class":147},[134,625,626],{"class":406},"push",[134,628,410],{"class":498},[134,630,462],{"class":147},[134,632,633],{"class":151},"  --brand-",[134,635,473],{"class":147},[134,637,638],{"class":406},"kebab",[134,640,641],{"class":157},"(key)",[134,643,478],{"class":147},[134,645,158],{"class":151},[134,647,473],{"class":147},[134,649,650],{"class":157},"value",[134,652,478],{"class":147},[134,654,612],{"class":151},[134,656,462],{"class":147},[134,658,422],{"class":498},[134,660,486],{"class":147},[134,662,664],{"class":136,"line":663},8,[134,665,666],{"class":147},"  }\n",[134,668,670,673,675,677,679,681,683,685,687],{"class":136,"line":669},9,[134,671,672],{"class":157},"  lines",[134,674,332],{"class":147},[134,676,626],{"class":406},[134,678,410],{"class":498},[134,680,450],{"class":147},[134,682,478],{"class":151},[134,684,450],{"class":147},[134,686,422],{"class":498},[134,688,486],{"class":147},[134,690,692,695,697,699,702,704,706,709,711,713],{"class":136,"line":691},10,[134,693,694],{"class":521},"  return",[134,696,493],{"class":157},[134,698,332],{"class":147},[134,700,701],{"class":406},"join",[134,703,410],{"class":498},[134,705,450],{"class":147},[134,707,708],{"class":157},"\\n",[134,710,450],{"class":147},[134,712,422],{"class":498},[134,714,486],{"class":147},[134,716,718],{"class":136,"line":717},11,[134,719,271],{"class":147},[12,721,722,723,726],{},"результат — обычные CSS-переменные с каскадом тем (light — он же дефолт под ",[95,724,725],{},":root",", дальше переопределения):",[88,728,732],{"className":729,"code":730,"language":731,"meta":97,"style":97},"language-css shiki shiki-themes material-theme-lighter material-theme material-theme-palenight",":root,\n[data-theme='light'] {\n  --brand-accent: #635bff;\n}\n[data-theme='dark'],\n.dark {\n  --brand-accent: #7b75ff;\n}\n","css",[95,733,734,744,764,779,783,800,808,821],{"__ignoreMap":97},[134,735,736,738,741],{"class":136,"line":137},[134,737,86],{"class":147},[134,739,740],{"class":170},"root",[134,742,743],{"class":147},",\n",[134,745,746,748,751,754,756,758,760,762],{"class":136,"line":144},[134,747,589],{"class":147},[134,749,750],{"class":170},"data-theme",[134,752,753],{"class":147},"=",[134,755,450],{"class":147},[134,757,171],{"class":151},[134,759,450],{"class":147},[134,761,514],{"class":147},[134,763,425],{"class":147},[134,765,766,769,771,774,777],{"class":136,"line":164},[134,767,768],{"class":157},"  --brand-accent",[134,770,86],{"class":147},[134,772,773],{"class":147}," #",[134,775,776],{"class":157},"635bff",[134,778,486],{"class":147},[134,780,781],{"class":136,"line":219},[134,782,271],{"class":147},[134,784,785,787,789,791,793,795,797],{"class":136,"line":268},[134,786,589],{"class":147},[134,788,750],{"class":170},[134,790,753],{"class":147},[134,792,450],{"class":147},[134,794,224],{"class":151},[134,796,450],{"class":147},[134,798,799],{"class":147},"],\n",[134,801,802,804,806],{"class":136,"line":573},[134,803,332],{"class":147},[134,805,224],{"class":184},[134,807,425],{"class":147},[134,809,810,812,814,816,819],{"class":136,"line":618},[134,811,768],{"class":157},[134,813,86],{"class":147},[134,815,773],{"class":147},[134,817,818],{"class":157},"7b75ff",[134,820,486],{"class":147},[134,822,823],{"class":136,"line":663},[134,824,271],{"class":147},[12,826,827],{},"из одного источника эмиттер выдаёт все темы разом (тут — light \u002F dark \u002F sepia \u002F forest) — то есть тёмная тема и ребрендинг достаются «бесплатно».",[12,829,830],{},"сборка веером пишет артефакты под каждую платформу одной командой:",[88,832,834],{"className":389,"code":833,"language":391,"meta":97,"style":97},"\u002F\u002F упрощённо из packages\u002Fdesign-tokens\u002Fsrc\u002Fbuild.ts\nconst targets = [\n  { file: 'apps\u002Fweb\u002Fapp\u002Fassets\u002Fcss\u002Ftokens.generated.css', contents: emitScss(tokens) },\n  { file: 'packages\u002Fui\u002Fsrc\u002Ftokens.generated.css', contents: emitScss(tokens) },\n  { file: 'apps\u002Fweb\u002Fapp\u002Fdesign-tokens.generated.ts', contents: emitTypescript(tokens) },\n  { file: 'packages\u002Fui\u002Fsrc\u002Fdesign-tokens.generated.ts', contents: emitTypescript(tokens) },\n  { file: 'packages\u002Fui_flutter\u002Flib\u002Fsrc\u002Ftheme\u002Ftokens.g.dart', contents: emitDart(tokens) },\n];\n",[95,835,836,841,853,885,912,940,967,995],{"__ignoreMap":97},[134,837,838],{"class":136,"line":137},[134,839,840],{"class":140},"\u002F\u002F упрощённо из packages\u002Fdesign-tokens\u002Fsrc\u002Fbuild.ts\n",[134,842,843,845,848,850],{"class":136,"line":144},[134,844,528],{"class":170},[134,846,847],{"class":157}," targets ",[134,849,753],{"class":147},[134,851,852],{"class":157}," [\n",[134,854,855,857,860,862,864,867,869,871,874,876,879,882],{"class":136,"line":164},[134,856,231],{"class":147},[134,858,859],{"class":498}," file",[134,861,86],{"class":147},[134,863,445],{"class":147},[134,865,866],{"class":151},"apps\u002Fweb\u002Fapp\u002Fassets\u002Fcss\u002Ftokens.generated.css",[134,868,450],{"class":147},[134,870,198],{"class":147},[134,872,873],{"class":498}," contents",[134,875,86],{"class":147},[134,877,878],{"class":406}," emitScss",[134,880,881],{"class":157},"(tokens) ",[134,883,884],{"class":147},"},\n",[134,886,887,889,891,893,895,898,900,902,904,906,908,910],{"class":136,"line":219},[134,888,231],{"class":147},[134,890,859],{"class":498},[134,892,86],{"class":147},[134,894,445],{"class":147},[134,896,897],{"class":151},"packages\u002Fui\u002Fsrc\u002Ftokens.generated.css",[134,899,450],{"class":147},[134,901,198],{"class":147},[134,903,873],{"class":498},[134,905,86],{"class":147},[134,907,878],{"class":406},[134,909,881],{"class":157},[134,911,884],{"class":147},[134,913,914,916,918,920,922,925,927,929,931,933,936,938],{"class":136,"line":268},[134,915,231],{"class":147},[134,917,859],{"class":498},[134,919,86],{"class":147},[134,921,445],{"class":147},[134,923,924],{"class":151},"apps\u002Fweb\u002Fapp\u002Fdesign-tokens.generated.ts",[134,926,450],{"class":147},[134,928,198],{"class":147},[134,930,873],{"class":498},[134,932,86],{"class":147},[134,934,935],{"class":406}," emitTypescript",[134,937,881],{"class":157},[134,939,884],{"class":147},[134,941,942,944,946,948,950,953,955,957,959,961,963,965],{"class":136,"line":573},[134,943,231],{"class":147},[134,945,859],{"class":498},[134,947,86],{"class":147},[134,949,445],{"class":147},[134,951,952],{"class":151},"packages\u002Fui\u002Fsrc\u002Fdesign-tokens.generated.ts",[134,954,450],{"class":147},[134,956,198],{"class":147},[134,958,873],{"class":498},[134,960,86],{"class":147},[134,962,935],{"class":406},[134,964,881],{"class":157},[134,966,884],{"class":147},[134,968,969,971,973,975,977,980,982,984,986,988,991,993],{"class":136,"line":618},[134,970,231],{"class":147},[134,972,859],{"class":498},[134,974,86],{"class":147},[134,976,445],{"class":147},[134,978,979],{"class":151},"packages\u002Fui_flutter\u002Flib\u002Fsrc\u002Ftheme\u002Ftokens.g.dart",[134,981,450],{"class":147},[134,983,198],{"class":147},[134,985,873],{"class":498},[134,987,86],{"class":147},[134,989,990],{"class":406}," emitDart",[134,992,881],{"class":157},[134,994,884],{"class":147},[134,996,997,999],{"class":136,"line":663},[134,998,514],{"class":157},[134,1000,486],{"class":147},[12,1002,1003,1004,1007],{},"вот наглядный пруф переносимости — ",[42,1005,1006],{},"один и тот же токен"," на двух несвязанных рантаймах:",[88,1009,1011],{"className":729,"code":1010,"language":731,"meta":97,"style":97},"\u002F* CSS \u002F SCSS (браузер) *\u002F\n--brand-accent: #635bff;\n",[95,1012,1013,1018],{"__ignoreMap":97},[134,1014,1015],{"class":136,"line":137},[134,1016,1017],{"class":140},"\u002F* CSS \u002F SCSS (браузер) *\u002F\n",[134,1019,1020,1023,1026],{"class":136,"line":144},[134,1021,1022],{"class":157},"--brand-accent: ",[134,1024,1025],{"class":147},"#",[134,1027,1028],{"class":157},"635bff;\n",[88,1030,1034],{"className":1031,"code":1032,"language":1033,"meta":97,"style":97},"language-dart shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","\u002F\u002F Flutter \u002F Dart (мобильный рантайм)\nstatic const Color brandAccent = Color(0xFF635BFF);\n","dart",[95,1035,1036,1041],{"__ignoreMap":97},[134,1037,1038],{"class":136,"line":137},[134,1039,1040],{},"\u002F\u002F Flutter \u002F Dart (мобильный рантайм)\n",[134,1042,1043],{"class":136,"line":144},[134,1044,1045],{},"static const Color brandAccent = Color(0xFF635BFF);\n",[12,1047,1048,1049,1052,1053,1056,1057,332],{},"переносим именно ",[22,1050,1051],{},"подход",": хочешь новый стек — добавляешь эмиттер под новый таргет, ",[42,1054,1055],{},"источник не трогаешь",". принцип целиком: ",[22,1058,1059],{},"генерация в нативный формат каждой платформы",[28,1061,1062],{},[12,1063,1064,1065,1072,1073,332],{},"→ эмиттеры целиком: ",[34,1066,1069],{"href":1067,"rel":1068},"https:\u002F\u002Fgithub.com\u002Fevgentus-cy\u002Fclaude-driven-nest-nuxt-flutter-monorepo\u002Ftree\u002Fmain\u002Fpackages\u002Fdesign-tokens",[38],[95,1070,1071],{},"packages\u002Fdesign-tokens"," · те же токены, доехавшие до мобильного рантайма: ",[34,1074,1077],{"href":1075,"rel":1076},"https:\u002F\u002Fgithub.com\u002Fevgentus-cy\u002Fclaude-driven-nest-nuxt-flutter-monorepo\u002Ftree\u002Fmain\u002Fpackages\u002Fui_flutter",[38],[95,1078,1079],{},"packages\u002Fui_flutter",[51,1081,1083],{"id":1082},"шаг-3-примитивы-на-токенах-а-не-обёртки-над-чужим-китом","шаг 3 · примитивы — на токенах, а не обёртки над чужим китом",[12,1085,1086,1087,1090,1091,1094],{},"соблазн — взять готовый UI-кит и обернуть его кнопку. но тогда стили живут ",[22,1088,1089],{},"внутри"," чужого кода, и правила на них не навесишь. поэтому базовые примитивы — ",[42,1092,1093],{},"свои",", со стилями только на токенах.",[12,1096,1097],{},"каждый компонент — это папка с полным комплектом (об этом — на шаге 4):",[88,1099,1102],{"className":1100,"code":1101,"language":93},[91],"packages\u002Fui\u002Fsrc\u002Fcomponents\u002FAppButton\u002F\n  AppButton.vue        # разметка + стили на токенах\n  AppButton.stories.ts # витрина всех вариантов\n  AppButton.spec.ts    # рендер + a11y + варианты\n  index.ts\n",[95,1103,1101],{"__ignoreMap":97},[12,1105,1106,1107,1110],{},"стили ссылаются только на ",[95,1108,1109],{},"var(--*)",", классы — по BEM с префиксом, никаких сырых значений:",[88,1112,1116],{"className":1113,"code":1114,"language":1115,"meta":97,"style":97},"language-scss shiki shiki-themes material-theme-lighter material-theme material-theme-palenight",".app-button {\n  height: var(--space-10);\n  padding: 0 var(--space-4);\n  border-radius: var(--radius-lg);\n  font-size: var(--text-md);\n  transition: background var(--dur-base) var(--ease);\n\n  &--primary {\n    \u002F\u002F BEM-модификатор\n    background: var(--brand-primary);\n    color: var(--brand-primary-fg);\n  }\n}\n","scss",[95,1117,1118,1127,1146,1166,1182,1198,1227,1233,1243,1248,1264,1280,1285],{"__ignoreMap":97},[134,1119,1120,1122,1125],{"class":136,"line":137},[134,1121,332],{"class":147},[134,1123,1124],{"class":184},"app-button",[134,1126,425],{"class":147},[134,1128,1129,1133,1135,1138,1140,1143],{"class":136,"line":144},[134,1130,1132],{"class":1131},"sqsOY","  height",[134,1134,86],{"class":147},[134,1136,1137],{"class":406}," var",[134,1139,410],{"class":147},[134,1141,1142],{"class":157},"--space-10",[134,1144,1145],{"class":147},");\n",[134,1147,1148,1151,1153,1157,1159,1161,1164],{"class":136,"line":164},[134,1149,1150],{"class":1131},"  padding",[134,1152,86],{"class":147},[134,1154,1156],{"class":1155},"sbssI"," 0",[134,1158,1137],{"class":406},[134,1160,410],{"class":147},[134,1162,1163],{"class":157},"--space-4",[134,1165,1145],{"class":147},[134,1167,1168,1171,1173,1175,1177,1180],{"class":136,"line":219},[134,1169,1170],{"class":1131},"  border-radius",[134,1172,86],{"class":147},[134,1174,1137],{"class":406},[134,1176,410],{"class":147},[134,1178,1179],{"class":157},"--radius-lg",[134,1181,1145],{"class":147},[134,1183,1184,1187,1189,1191,1193,1196],{"class":136,"line":268},[134,1185,1186],{"class":1131},"  font-size",[134,1188,86],{"class":147},[134,1190,1137],{"class":406},[134,1192,410],{"class":147},[134,1194,1195],{"class":157},"--text-md",[134,1197,1145],{"class":147},[134,1199,1200,1203,1205,1208,1211,1213,1216,1218,1220,1222,1225],{"class":136,"line":573},[134,1201,1202],{"class":1131},"  transition",[134,1204,86],{"class":147},[134,1206,1207],{"class":157}," background ",[134,1209,1210],{"class":406},"var",[134,1212,410],{"class":147},[134,1214,1215],{"class":157},"--dur-base",[134,1217,422],{"class":147},[134,1219,1137],{"class":406},[134,1221,410],{"class":147},[134,1223,1224],{"class":157},"--ease",[134,1226,1145],{"class":147},[134,1228,1229],{"class":136,"line":618},[134,1230,1232],{"emptyLinePlaceholder":1231},true,"\n",[134,1234,1235,1238,1241],{"class":136,"line":663},[134,1236,1237],{"class":184},"  &",[134,1239,1240],{"class":157},"--primary ",[134,1242,161],{"class":147},[134,1244,1245],{"class":136,"line":669},[134,1246,1247],{"class":140},"    \u002F\u002F BEM-модификатор\n",[134,1249,1250,1253,1255,1257,1259,1262],{"class":136,"line":691},[134,1251,1252],{"class":1131},"    background",[134,1254,86],{"class":147},[134,1256,1137],{"class":406},[134,1258,410],{"class":147},[134,1260,1261],{"class":157},"--brand-primary",[134,1263,1145],{"class":147},[134,1265,1266,1269,1271,1273,1275,1278],{"class":136,"line":717},[134,1267,1268],{"class":1131},"    color",[134,1270,86],{"class":147},[134,1272,1137],{"class":406},[134,1274,410],{"class":147},[134,1276,1277],{"class":157},"--brand-primary-fg",[134,1279,1145],{"class":147},[134,1281,1283],{"class":136,"line":1282},12,[134,1284,666],{"class":147},[134,1286,1288],{"class":136,"line":1287},13,[134,1289,271],{"class":147},[12,1291,1292,1293,1296],{},"api компонента читается как намерение: ",[95,1294,1295],{},"color=\"primary\"",", а не цвет. поэтому ребрендинг из шага 1 проходит сквозь весь UI, не задевая ни одной сигнатуры.",[12,1298,1299,1300,1303,1304,1307],{},"полностью убежать от чужого кита нельзя — сложные поведенческие виджеты (модалка, дропдаун, таблица, календарь) дешевле взять готовыми. их ",[42,1301,1302],{},"не запрещают",", а ",[22,1305,1306],{},"изолируют",": чужой символ импортируется ровно в одном месте — в своей обёртке-примитиве, и наружу торчит только твой компонент.",[12,1309,1310,1311,1314],{},"принцип: ",[22,1312,1313],{},"владей примитивами"," — правила можно проверять только на коде, которым владеешь.",[28,1316,1317],{},[12,1318,1319,1320,332],{},"→ библиотека примитивов: ",[34,1321,1324],{"href":1322,"rel":1323},"https:\u002F\u002Fgithub.com\u002Fevgentus-cy\u002Fclaude-driven-nest-nuxt-flutter-monorepo\u002Ftree\u002Fmain\u002Fpackages\u002Fui",[38],[95,1325,1326],{},"packages\u002Fui",[51,1328,1330],{"id":1329},"шаг-4-зубы-машинные-правила-вместо-гайдлайна","шаг 4 · зубы: машинные правила вместо гайдлайна",[12,1332,1333,1334,332],{},"это шаг, ради которого всё затевалось. правила — не строчки в вики, а конфиги, которые ",[42,1335,1336],{},"валят сборку",[12,1338,1339,1342],{},[42,1340,1341],{},"линтер запрещает хардкод."," реальные правила:",[88,1344,1348],{"className":1345,"code":1346,"language":1347,"meta":97,"style":97},"language-js shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","\u002F\u002F упрощённо из stylelint.config.mjs\nrules: {\n  'declaration-no-important': true,        \u002F\u002F никаких !important\n  'color-no-hex': true,                    \u002F\u002F никаких #635bff — только var(--brand-*)\n  'color-named': 'never',                  \u002F\u002F никаких red \u002F blue\n  \u002F\u002F плюс: z-index только var(--z-*), длительности — var(--dur-*),\n  \u002F\u002F       отступы ≥3px — var(--space-*); классы — BEM с префиксом app-\u002Fhealth-\u002Fbrand-\n}\n","js",[95,1349,1350,1355,1364,1385,1403,1426,1431,1436],{"__ignoreMap":97},[134,1351,1352],{"class":136,"line":137},[134,1353,1354],{"class":140},"\u002F\u002F упрощённо из stylelint.config.mjs\n",[134,1356,1357,1360,1362],{"class":136,"line":144},[134,1358,1359],{"class":184},"rules",[134,1361,86],{"class":147},[134,1363,425],{"class":147},[134,1365,1366,1369,1372,1374,1376,1380,1382],{"class":136,"line":164},[134,1367,1368],{"class":147},"  '",[134,1370,1371],{"class":151},"declaration-no-important",[134,1373,450],{"class":147},[134,1375,158],{"class":498},[134,1377,1379],{"class":1378},"sfNiH","true",[134,1381,198],{"class":147},[134,1383,1384],{"class":140},"        \u002F\u002F никаких !important\n",[134,1386,1387,1389,1392,1394,1396,1398,1400],{"class":136,"line":219},[134,1388,1368],{"class":147},[134,1390,1391],{"class":151},"color-no-hex",[134,1393,450],{"class":147},[134,1395,158],{"class":498},[134,1397,1379],{"class":1378},[134,1399,198],{"class":147},[134,1401,1402],{"class":140},"                    \u002F\u002F никаких #635bff — только var(--brand-*)\n",[134,1404,1405,1407,1410,1412,1414,1416,1419,1421,1423],{"class":136,"line":268},[134,1406,1368],{"class":147},[134,1408,1409],{"class":151},"color-named",[134,1411,450],{"class":147},[134,1413,158],{"class":498},[134,1415,450],{"class":147},[134,1417,1418],{"class":151},"never",[134,1420,450],{"class":147},[134,1422,198],{"class":147},[134,1424,1425],{"class":140},"                  \u002F\u002F никаких red \u002F blue\n",[134,1427,1428],{"class":136,"line":573},[134,1429,1430],{"class":140},"  \u002F\u002F плюс: z-index только var(--z-*), длительности — var(--dur-*),\n",[134,1432,1433],{"class":136,"line":618},[134,1434,1435],{"class":140},"  \u002F\u002F       отступы ≥3px — var(--space-*); классы — BEM с префиксом app-\u002Fhealth-\u002Fbrand-\n",[134,1437,1438],{"class":136,"line":663},[134,1439,271],{"class":147},[12,1441,1442,1443,1446,1447,1449],{},"захардкодил ",[95,1444,1445],{},"#F26A1F"," вместо ",[95,1448,152],{}," — линт это отвергает. не абстрактная договорённость, а отказ.",[12,1451,1452,1455],{},[42,1453,1454],{},"аудит требует полный комплект у компонента."," скрипт обходит папки и падает, если у компонента нет истории или спеки:",[88,1457,1459],{"className":389,"code":1458,"language":391,"meta":97,"style":97},"\u002F\u002F суть packages\u002Fui\u002Fscripts\u002Faudit-components.ts\n\u002F\u002F для каждой папки компонента обязаны существовать:\n\u002F\u002F   \u003CName>.vue, \u003CName>.stories.ts, \u003CName>.spec.ts, index.ts\n\u002F\u002F иначе process.exit(1) — «component audit failed».\n",[95,1460,1461,1466,1471,1476],{"__ignoreMap":97},[134,1462,1463],{"class":136,"line":137},[134,1464,1465],{"class":140},"\u002F\u002F суть packages\u002Fui\u002Fscripts\u002Faudit-components.ts\n",[134,1467,1468],{"class":136,"line":144},[134,1469,1470],{"class":140},"\u002F\u002F для каждой папки компонента обязаны существовать:\n",[134,1472,1473],{"class":136,"line":164},[134,1474,1475],{"class":140},"\u002F\u002F   \u003CName>.vue, \u003CName>.stories.ts, \u003CName>.spec.ts, index.ts\n",[134,1477,1478],{"class":136,"line":219},[134,1479,1480],{"class":140},"\u002F\u002F иначе process.exit(1) — «component audit failed».\n",[12,1482,1483,1484,1487],{},"этот аудит реально крутится в CI (job ",[95,1485,1486],{},"ui-quality","), так что «компонент без витрины и теста» физически не доезжает до main.",[12,1489,1490,1493,1494,1497,1498,1501],{},[42,1491,1492],{},"CI ловит дрейф сгенерированного кода."," артефакты закоммичены; CI перегенерирует их и сравнивает с деревом — расхождение валит PR. в этой репе drift-gate сейчас стоит на сгенерированных API-клиентах (",[95,1495,1496],{},"spec:codegen"," → ",[95,1499,1500],{},"git diff --exit-code","); для дизайн-токенов тот же приём — следующий очевидный шаг (см. «что дальше»). пока токены держит запрет ручной правки генерёнок и регенерация на месте.",[12,1503,1310,1504,1507],{},[22,1505,1506],{},"правила машинные, а не в гайдлайне",". гайдлайн — это надежда; падающий пайплайн — гарантия. и именно это превращает «агент пишет фичи» из риска в управляемый процесс.",[28,1509,1510],{},[12,1511,1512,1513,332],{},"→ реальный конфиг правил: ",[34,1514,1517],{"href":1515,"rel":1516},"https:\u002F\u002Fgithub.com\u002Fevgentus-cy\u002Fclaude-driven-nest-nuxt-flutter-monorepo\u002Fblob\u002Fmain\u002Fstylelint.config.mjs",[38],[95,1518,1519],{},"stylelint.config.mjs",[51,1521,1523],{"id":1522},"шаг-5-поток-изменений-всегда-в-одну-сторону","шаг 5 · поток изменений — всегда в одну сторону",[12,1525,1526],{},"связываем всё в цикл. любое дизайн-изменение идёт строго сверху вниз:",[276,1528,1529,1538,1547,1553],{},[279,1530,1531,1534,1535,332],{},[42,1532,1533],{},"правишь токен"," — ",[95,1536,1537],{},"specs\u002Fdesign\u002Ftokens\u002F*.json",[279,1539,1540,1534,1543,1546],{},[42,1541,1542],{},"регенерируешь",[95,1544,1545],{},"pnpm design:build",". артефакты под web \u002F Storybook \u002F mobile пересобираются разом.",[279,1548,1549,1552],{},[42,1550,1551],{},"обновляешь или создаёшь компонент"," в библиотеке (с историей и спекой).",[279,1554,1555,1558],{},[42,1556,1557],{},"потребляешь"," в приложениях — страницы остаются тонкими: данные, пропсы, роутинг.",[12,1560,1561,1562,1565],{},"правило: ",[22,1563,1564],{},"никогда не правишь «вниз по течению», чтобы починить значение «выше»",". если компонент рендерит старый цвет — он использует литерал вместо токена; чинишь компонент, а не подгоняешь токен. обратная правка — это баг, а не гибкость.",[51,1567,1569],{"id":1568},"честные-ограничения","честные ограничения",[12,1571,1572],{},"подход не бесплатный — и это нормально:",[315,1574,1575,1581,1587,1597,1603],{},[279,1576,1577,1580],{},[42,1578,1579],{},"регенерация — реальный шаг, а артефакты коммитятся."," забыл пересобрать — получаешь дрейф. сейчас токены держит запрет ручной правки генерёнок; CI-drift-gate (уже закрывающий сгенерированные клиенты) на токены пока не распространён — см. «что дальше».",[279,1582,1583,1586],{},[42,1584,1585],{},"токен — это контракт."," переименовать или удалить токен = breaking change для всех потребителей на всех платформах. только осознанно (в репе — через ADR).",[279,1588,1589,1592,1593,1596],{},[42,1590,1591],{},"свои примитивы дороже в моменте",", чем ",[95,1594,1595],{},"install"," готового кита.",[279,1598,1599,1602],{},[42,1600,1601],{},"от чужого кита не убежать полностью"," — сложные виджеты всё равно на нём, просто за обёрткой.",[279,1604,1605,1608],{},[42,1606,1607],{},"налог на строгость."," «никаких magic numbers» замедляет «просто быстро накидать».",[51,1610,1612],{"id":1611},"что-дальше-что-можно-улучшить","что дальше \u002F что можно улучшить",[12,1614,1615,1616,1619,1620,1623],{},"направление одно: каждый следующий шаг ",[42,1617,1618],{},"сужает разрыв между намерением и проверкой"," или ",[42,1621,1622],{},"укорачивает петлю"," источник → продукт.",[315,1625,1626,1632,1641,1651,1657],{},[279,1627,1628,1631],{},[42,1629,1630],{},"замкнуть петлю дизайнер↔код"," — тянуть токены прямо из дизайн-инструмента через тот же стандарт (DTCG ровно для этого и создан), чтобы источник перестал быть рукописным JSON.",[279,1633,1634,1637,1638,1640],{},[42,1635,1636],{},"сделать регенерацию токенов незабываемой"," — drift-gate в CI (как уже сделано для сгенерированных клиентов через ",[95,1639,1496],{},"), который регенерит токены и падает на расхождении. закрывает ограничение №1.",[279,1642,1643,1646,1647,1650],{},[42,1644,1645],{},"алиасы токенов"," — семантические токены ссылаются на примитивы (",[95,1648,1649],{},"{group.token}"," по DTCG), убирая дублирование и ещё удешевляя ребрендинг.",[279,1652,1653,1656],{},[42,1654,1655],{},"codemod на переименование"," — превратить цену «токен = контракт» из ручной в механическую.",[279,1658,1659,1662],{},[42,1660,1661],{},"шире машинная проверка"," — контракт-тест «каждая роль × каждая тема имеет значение»; визуальные baseline-снимки на каждый компонент.",[51,1664,1665],{"id":1665},"итог",[12,1667,1668,1669,1672],{},"дизайн-слой здесь — не библиотека, а ",[42,1670,1671],{},"машина инвариантов",": один нейтральный источник, генерация под любой стек и набор линтеров и аудитов, которые делают нарушение правил технически невозможным. много источников дизайна сходятся в один стандартный формат, из него — много таргетов; а правила держат систему целостной, кто бы ни писал код.",[12,1674,1675,1676,1680,1681,332],{},"→ посмотреть всё вживую: ",[34,1677,1679],{"href":36,"rel":1678},[38],"репозиторий-витрина"," · стандарт формата токенов — ",[34,1682,1684],{"href":114,"rel":1683},[38],"designtokens.org",[12,1686,1687],{},[22,1688,1689],{},"дальше в серии — спек-first контур: как OpenAPI + кодоген не дают бэкенду и клиентам разъехаться по контракту.",[1691,1692,1693],"style",{},"html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sqsOY, html code.shiki .sqsOY{--shiki-light:#8796B0;--shiki-default:#B2CCD6;--shiki-dark:#B2CCD6}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}",{"title":97,"searchDepth":144,"depth":164,"links":1695},[1696],{"id":18,"depth":144,"text":5,"children":1697},[1698,1699,1700,1701,1702,1703,1704,1705,1706,1707],{"id":53,"depth":164,"text":54},{"id":78,"depth":164,"text":79},{"id":103,"depth":164,"text":104},{"id":371,"depth":164,"text":372},{"id":1082,"depth":164,"text":1083},{"id":1329,"depth":164,"text":1330},{"id":1522,"depth":164,"text":1523},{"id":1568,"depth":164,"text":1569},{"id":1611,"depth":164,"text":1612},{"id":1665,"depth":164,"text":1665},"2026-06-13","часть 1 серии про claude-driven монорепу: как нейтральный источник токенов плюс машинно-проверяемые правила делают инварианты дизайн-системы невозможными для нарушения — хоть руками, хоть агентом.","md","лонгрид",{},"015","\u002Fwriting\u002F015-design-system-invariants","12 мин",{"title":5,"description":1709},"015-design-system-invariants","writing\u002F015-design-system-invariants",{"label":1711,"color":1720},"var(--red)","xnQikwGapp7uBf7BS06XPUk5ZtHoxD8C2pJgx3oneoo",{"id":1723,"title":1724,"bars":6,"blurb":1725,"body":1726,"color":6,"contacts":6,"date":1708,"dateModified":6,"description":3186,"extension":1710,"featured":1231,"groups":6,"kicker":3187,"meta":3188,"metrics":6,"n":1713,"navigation":1231,"openTabs":6,"path":1714,"progress":6,"readTime":3189,"reading":6,"role":6,"rules":6,"running":6,"seo":3190,"ships":6,"slug":1717,"stack":6,"started":6,"status":6,"stem":1718,"streak":6,"tag":3191,"tagColor":1720,"tagline":6,"tasks":6,"timeline":6,"topics":6,"week":6,"year":6,"__hash__":3192},"en\u002Fwriting\u002F015-design-system-invariants.md","Design systems as invariants: building a token pipeline from scratch","part 1 of the claude-driven monorepo series — a design layer whose rules make the wrong thing impossible, not just discouraged.",{"type":9,"value":1727,"toc":3172},[1728,1731,1734,1738,1740,1757,1761,1764,1767,1779,1782,1786,1793,1799,1802,1806,1822,1934,1937,1966,1969,1994,2013,2017,2028,2031,2294,2300,2386,2389,2392,2548,2555,2573,2587,2601,2617,2621,2632,2635,2641,2647,2795,2801,2812,2819,2829,2833,2839,2845,2932,2941,2947,2971,2977,2988,2994,3004,3008,3011,3040,3047,3051,3054,3089,3093,3104,3142,3146,3153,3164,3169],[12,1729,1730],{},"▸ featured · long read — № 015 · jun 13 '26 · 12 min · series: claude-driven monorepo · pt.1",[16,1732,1724],{"id":1733},"design-systems-as-invariants-building-a-token-pipeline-from-scratch",[12,1735,1736],{},[22,1737,1725],{},[25,1739],{},[28,1741,1742],{},[12,1743,1744,1745,1749,1750,1753,1754,1756],{},"this is the first piece in a series on the principles behind a ",[34,1746,1748],{"href":36,"rel":1747},[38],"claude-driven monorepo",".\nthe repo bills itself as a ",[42,1751,1752],{},"\"spec-first, agent-friendly monorepo boilerplate\""," — a production-shaped starter for teams building in the ",[42,1755,48],{}," loop: working backend \u002F web \u002F mobile, one shared design-token pipeline, a single source of truth for every contract, and drift-guards that make agent-driven work safe \"without a human over every keystroke\". today is the design layer — in detail, step by step, so you can build the same pipeline yourself. the stack (NestJS \u002F Nuxt \u002F Flutter) is just a worked example; the approach ports to any of them.",[51,1758,1760],{"id":1759},"why-invariants-at-all","why invariants at all",[12,1762,1763],{},"greenfield projects with an AI assistant fail in predictable ways: the agent drops a hardcoded color into a component, renames a field in one client and forgets the other, changes something \"in place\". by the time you review, the diff is already dozens of files deep, and the human catches the drift too late.",[12,1765,1766],{},"the conclusion the whole approach rests on:",[28,1768,1769],{},[12,1770,1771,1774,1775,1778],{},[42,1772,1773],{},"a design system isn't a component library — it's a set of machine-checkable invariants.","\nyou make the wrong thing ",[22,1776,1777],{},"impossible",", not \"discouraged\". then it doesn't matter who writes the code — human or agent: the rules hold both the same.",[12,1780,1781],{},"here's how to build one, step by step.",[51,1783,1785],{"id":1784},"the-whole-picture","the whole picture",[12,1787,1788,1789,1792],{},"one neutral format in the center, with ",[42,1790,1791],{},"both inputs and outputs"," converging on it:",[88,1794,1797],{"className":1795,"code":1796,"language":93},[91],"  design sources                                     stack targets\n  ──────────────                                     ─────────────\n  Claude Design  ┐                              ┌─►  CSS \u002F SCSS variables\n  Figma          ├─►  [ TOKENS: W3C DTCG ]  ──► emitters ─┼─►  TypeScript constants\n  Penpot         ├─►   single source        (per target)  ├─►  Flutter \u002F Dart constants\n  by hand (JSON) ┘                              └─►  …any other\n                          │\n                          ▼\n                  primitives library on tokens  ──►  product screens\n                          │\n                          ▼\n                  machine rules (linters · audit · CI drift)\n",[95,1798,1796],{"__ignoreMap":97},[12,1800,1801],{},"now each block, one at a time.",[51,1803,1805],{"id":1804},"step-1-the-source-tokens-in-a-neutral-format","step 1 · the source: tokens in a neutral format",[12,1807,1808,1809,1812,1813,1816,1817,121,1819,1821],{},"the source of truth is ",[42,1810,1811],{},"semantic tokens"," in the ",[34,1814,116],{"href":114,"rel":1815},[38]," format. the format deliberately knows nothing about any framework: a leaf is a ",[95,1818,120],{},[95,1820,124],{}," pair, plus theme pairs.",[88,1823,1824],{"className":128,"code":129,"language":130,"meta":97,"style":97},[95,1825,1826,1830,1842,1886,1930],{"__ignoreMap":97},[134,1827,1828],{"class":136,"line":137},[134,1829,141],{"class":140},[134,1831,1832,1834,1836,1838,1840],{"class":136,"line":144},[134,1833,148],{"class":147},[134,1835,152],{"class":151},[134,1837,148],{"class":147},[134,1839,158],{"class":157},[134,1841,161],{"class":147},[134,1843,1844,1846,1848,1850,1852,1854,1856,1858,1860,1862,1864,1866,1868,1870,1872,1874,1876,1878,1880,1882,1884],{"class":136,"line":164},[134,1845,167],{"class":147},[134,1847,171],{"class":170},[134,1849,148],{"class":147},[134,1851,86],{"class":147},[134,1853,178],{"class":147},[134,1855,181],{"class":147},[134,1857,120],{"class":184},[134,1859,148],{"class":147},[134,1861,86],{"class":147},[134,1863,181],{"class":147},[134,1865,193],{"class":151},[134,1867,148],{"class":147},[134,1869,198],{"class":147},[134,1871,181],{"class":147},[134,1873,124],{"class":184},[134,1875,148],{"class":147},[134,1877,86],{"class":147},[134,1879,181],{"class":147},[134,1881,211],{"class":151},[134,1883,148],{"class":147},[134,1885,216],{"class":147},[134,1887,1888,1890,1892,1894,1896,1898,1900,1902,1904,1906,1908,1910,1912,1914,1916,1918,1920,1922,1924,1926,1928],{"class":136,"line":219},[134,1889,167],{"class":147},[134,1891,224],{"class":170},[134,1893,148],{"class":147},[134,1895,86],{"class":147},[134,1897,231],{"class":147},[134,1899,181],{"class":147},[134,1901,120],{"class":184},[134,1903,148],{"class":147},[134,1905,86],{"class":147},[134,1907,181],{"class":147},[134,1909,244],{"class":151},[134,1911,148],{"class":147},[134,1913,198],{"class":147},[134,1915,181],{"class":147},[134,1917,124],{"class":184},[134,1919,148],{"class":147},[134,1921,86],{"class":147},[134,1923,181],{"class":147},[134,1925,211],{"class":151},[134,1927,148],{"class":147},[134,1929,265],{"class":147},[134,1931,1932],{"class":136,"line":268},[134,1933,271],{"class":147},[12,1935,1936],{},"two principles are baked in right here:",[276,1938,1939,1945],{},[279,1940,1941,1944],{},[42,1942,1943],{},"single source of truth."," every design decision (color, type, spacing, radii, shadows, motion, z-index) lives in one place — the token files. everything else is either generated from them or references them.",[279,1946,1947,1950,1951,293,1953,293,1955,1957,1958,121,1960,1962,1963,1965],{},[42,1948,1949],{},"name = intent, not value."," a token is called ",[95,1952,152],{},[95,1954,296],{},[95,1956,299],{}," — by its role, not ",[95,1959,193],{},[95,1961,305],{},". that's the superpower: a rebrand becomes a one-line edit. that's exactly what happened in this repo — the accent migrated ",[95,1964,309],{}," and not one component changed, because components reference the role, not the hex.",[12,1967,1968],{},"where the source itself comes from is up to you, and there are plenty of options (anything that can emit DTCG):",[315,1970,1971,1976,1983,1988],{},[279,1972,1973,1975],{},[42,1974,321],{}," — describe the brand in a brief, Claude generates the token JSON and the mockups. the repo ships a ready copy-paste brief for exactly this. this is \"AI as a design source\".",[279,1977,1978,1980,1981,332],{},[42,1979,327],{}," — export a token plugin to JSON, lay it out across ",[95,1982,331],{},[279,1984,1985,1987],{},[42,1986,337],{}," (the open-source Figma alternative) — the same plugin export.",[279,1989,1990,1993],{},[42,1991,1992],{},"by hand"," — just write the JSON.",[28,1995,1996],{},[12,1997,1998,1999,2004,2005,2007,2008,332],{},"→ source and format: ",[34,2000,2002],{"href":352,"rel":2001},[38],[95,2003,356],{}," (its ",[95,2006,360],{}," documents importing from Claude Design \u002F Figma \u002F Penpot \u002F by hand) · and ",[34,2009,2011],{"href":364,"rel":2010},[38],[95,2012,368],{},[51,2014,2016],{"id":2015},"step-2-the-emitter-one-source-many-targets","step 2 · the emitter: one source → many targets",[12,2018,2019,2020,2023,2024,2027],{},"this is the heart of portability. take one token model and ",[42,2021,2022],{},"compile"," it for each platform into that platform's ",[22,2025,2026],{},"native"," format. no manual \"duplicate the color in three places\" — parity is structural.",[12,2029,2030],{},"the CSS emitter walks the tokens and lays them out by theme (camelCase names → kebab-case):",[88,2032,2034],{"className":389,"code":2033,"language":391,"meta":97,"style":97},"\u002F\u002F simplified from packages\u002Fdesign-tokens\u002Fsrc\u002Femit-scss.ts\nfunction themedBlock(theme, tokens) {\n  const selector = theme === 'light' ? `:root, [data-theme='light']` : `[data-theme='${theme}']`;\n  const lines = [`${selector} {`];\n  for (const [key, leaf] of Object.entries(tokens.color.brand)) {\n    const value = (leaf[theme] ?? leaf.light).$value; \u002F\u002F fall back to light\n    lines.push(`  --brand-${kebab(key)}: ${value};`);\n  }\n  lines.push('}');\n  return lines.join('\\n');\n}\n",[95,2035,2036,2041,2059,2103,2127,2169,2206,2244,2248,2268,2290],{"__ignoreMap":97},[134,2037,2038],{"class":136,"line":137},[134,2039,2040],{"class":140},"\u002F\u002F simplified from packages\u002Fdesign-tokens\u002Fsrc\u002Femit-scss.ts\n",[134,2042,2043,2045,2047,2049,2051,2053,2055,2057],{"class":136,"line":144},[134,2044,403],{"class":170},[134,2046,407],{"class":406},[134,2048,410],{"class":147},[134,2050,414],{"class":413},[134,2052,198],{"class":147},[134,2054,419],{"class":413},[134,2056,422],{"class":147},[134,2058,425],{"class":147},[134,2060,2061,2063,2065,2067,2069,2071,2073,2075,2077,2079,2081,2083,2085,2087,2089,2091,2093,2095,2097,2099,2101],{"class":136,"line":164},[134,2062,430],{"class":170},[134,2064,433],{"class":157},[134,2066,436],{"class":147},[134,2068,439],{"class":157},[134,2070,442],{"class":147},[134,2072,445],{"class":147},[134,2074,171],{"class":151},[134,2076,450],{"class":147},[134,2078,453],{"class":147},[134,2080,456],{"class":147},[134,2082,459],{"class":151},[134,2084,462],{"class":147},[134,2086,465],{"class":147},[134,2088,456],{"class":147},[134,2090,470],{"class":151},[134,2092,473],{"class":147},[134,2094,414],{"class":157},[134,2096,478],{"class":147},[134,2098,481],{"class":151},[134,2100,462],{"class":147},[134,2102,486],{"class":147},[134,2104,2105,2107,2109,2111,2113,2115,2117,2119,2121,2123,2125],{"class":136,"line":219},[134,2106,430],{"class":170},[134,2108,493],{"class":157},[134,2110,436],{"class":147},[134,2112,499],{"class":498},[134,2114,502],{"class":147},[134,2116,505],{"class":157},[134,2118,478],{"class":147},[134,2120,178],{"class":151},[134,2122,462],{"class":147},[134,2124,514],{"class":498},[134,2126,486],{"class":147},[134,2128,2129,2131,2133,2135,2137,2139,2141,2143,2145,2147,2149,2151,2153,2155,2157,2159,2161,2163,2165,2167],{"class":136,"line":268},[134,2130,522],{"class":521},[134,2132,525],{"class":498},[134,2134,528],{"class":170},[134,2136,499],{"class":147},[134,2138,533],{"class":157},[134,2140,198],{"class":147},[134,2142,538],{"class":157},[134,2144,514],{"class":147},[134,2146,543],{"class":147},[134,2148,546],{"class":157},[134,2150,332],{"class":147},[134,2152,551],{"class":406},[134,2154,410],{"class":498},[134,2156,556],{"class":157},[134,2158,332],{"class":147},[134,2160,211],{"class":157},[134,2162,332],{"class":147},[134,2164,565],{"class":157},[134,2166,568],{"class":498},[134,2168,161],{"class":147},[134,2170,2171,2173,2175,2177,2179,2181,2183,2185,2187,2189,2191,2193,2195,2197,2199,2201,2203],{"class":136,"line":573},[134,2172,576],{"class":170},[134,2174,579],{"class":157},[134,2176,436],{"class":147},[134,2178,525],{"class":498},[134,2180,586],{"class":157},[134,2182,589],{"class":498},[134,2184,414],{"class":157},[134,2186,594],{"class":498},[134,2188,597],{"class":147},[134,2190,538],{"class":157},[134,2192,332],{"class":147},[134,2194,171],{"class":157},[134,2196,422],{"class":498},[134,2198,332],{"class":147},[134,2200,120],{"class":157},[134,2202,612],{"class":147},[134,2204,2205],{"class":140}," \u002F\u002F fall back to light\n",[134,2207,2208,2210,2212,2214,2216,2218,2220,2222,2224,2226,2228,2230,2232,2234,2236,2238,2240,2242],{"class":136,"line":618},[134,2209,621],{"class":157},[134,2211,332],{"class":147},[134,2213,626],{"class":406},[134,2215,410],{"class":498},[134,2217,462],{"class":147},[134,2219,633],{"class":151},[134,2221,473],{"class":147},[134,2223,638],{"class":406},[134,2225,641],{"class":157},[134,2227,478],{"class":147},[134,2229,158],{"class":151},[134,2231,473],{"class":147},[134,2233,650],{"class":157},[134,2235,478],{"class":147},[134,2237,612],{"class":151},[134,2239,462],{"class":147},[134,2241,422],{"class":498},[134,2243,486],{"class":147},[134,2245,2246],{"class":136,"line":663},[134,2247,666],{"class":147},[134,2249,2250,2252,2254,2256,2258,2260,2262,2264,2266],{"class":136,"line":669},[134,2251,672],{"class":157},[134,2253,332],{"class":147},[134,2255,626],{"class":406},[134,2257,410],{"class":498},[134,2259,450],{"class":147},[134,2261,478],{"class":151},[134,2263,450],{"class":147},[134,2265,422],{"class":498},[134,2267,486],{"class":147},[134,2269,2270,2272,2274,2276,2278,2280,2282,2284,2286,2288],{"class":136,"line":691},[134,2271,694],{"class":521},[134,2273,493],{"class":157},[134,2275,332],{"class":147},[134,2277,701],{"class":406},[134,2279,410],{"class":498},[134,2281,450],{"class":147},[134,2283,708],{"class":157},[134,2285,450],{"class":147},[134,2287,422],{"class":498},[134,2289,486],{"class":147},[134,2291,2292],{"class":136,"line":717},[134,2293,271],{"class":147},[12,2295,2296,2297,2299],{},"the result is plain CSS variables with a theme cascade (light is also the ",[95,2298,725],{}," default, the rest override):",[88,2301,2302],{"className":729,"code":730,"language":731,"meta":97,"style":97},[95,2303,2304,2312,2330,2342,2346,2362,2370,2382],{"__ignoreMap":97},[134,2305,2306,2308,2310],{"class":136,"line":137},[134,2307,86],{"class":147},[134,2309,740],{"class":170},[134,2311,743],{"class":147},[134,2313,2314,2316,2318,2320,2322,2324,2326,2328],{"class":136,"line":144},[134,2315,589],{"class":147},[134,2317,750],{"class":170},[134,2319,753],{"class":147},[134,2321,450],{"class":147},[134,2323,171],{"class":151},[134,2325,450],{"class":147},[134,2327,514],{"class":147},[134,2329,425],{"class":147},[134,2331,2332,2334,2336,2338,2340],{"class":136,"line":164},[134,2333,768],{"class":157},[134,2335,86],{"class":147},[134,2337,773],{"class":147},[134,2339,776],{"class":157},[134,2341,486],{"class":147},[134,2343,2344],{"class":136,"line":219},[134,2345,271],{"class":147},[134,2347,2348,2350,2352,2354,2356,2358,2360],{"class":136,"line":268},[134,2349,589],{"class":147},[134,2351,750],{"class":170},[134,2353,753],{"class":147},[134,2355,450],{"class":147},[134,2357,224],{"class":151},[134,2359,450],{"class":147},[134,2361,799],{"class":147},[134,2363,2364,2366,2368],{"class":136,"line":573},[134,2365,332],{"class":147},[134,2367,224],{"class":184},[134,2369,425],{"class":147},[134,2371,2372,2374,2376,2378,2380],{"class":136,"line":618},[134,2373,768],{"class":157},[134,2375,86],{"class":147},[134,2377,773],{"class":147},[134,2379,818],{"class":157},[134,2381,486],{"class":147},[134,2383,2384],{"class":136,"line":663},[134,2385,271],{"class":147},[12,2387,2388],{},"from one source the emitter rolls out every theme at once (here: light \u002F dark \u002F sepia \u002F forest) — so dark mode and rebrand come \"for free\".",[12,2390,2391],{},"a fan-out build writes the artifacts for each platform in one command:",[88,2393,2395],{"className":389,"code":2394,"language":391,"meta":97,"style":97},"\u002F\u002F simplified from packages\u002Fdesign-tokens\u002Fsrc\u002Fbuild.ts\nconst targets = [\n  { file: 'apps\u002Fweb\u002Fapp\u002Fassets\u002Fcss\u002Ftokens.generated.css', contents: emitScss(tokens) },\n  { file: 'packages\u002Fui\u002Fsrc\u002Ftokens.generated.css', contents: emitScss(tokens) },\n  { file: 'apps\u002Fweb\u002Fapp\u002Fdesign-tokens.generated.ts', contents: emitTypescript(tokens) },\n  { file: 'packages\u002Fui\u002Fsrc\u002Fdesign-tokens.generated.ts', contents: emitTypescript(tokens) },\n  { file: 'packages\u002Fui_flutter\u002Flib\u002Fsrc\u002Ftheme\u002Ftokens.g.dart', contents: emitDart(tokens) },\n];\n",[95,2396,2397,2402,2412,2438,2464,2490,2516,2542],{"__ignoreMap":97},[134,2398,2399],{"class":136,"line":137},[134,2400,2401],{"class":140},"\u002F\u002F simplified from packages\u002Fdesign-tokens\u002Fsrc\u002Fbuild.ts\n",[134,2403,2404,2406,2408,2410],{"class":136,"line":144},[134,2405,528],{"class":170},[134,2407,847],{"class":157},[134,2409,753],{"class":147},[134,2411,852],{"class":157},[134,2413,2414,2416,2418,2420,2422,2424,2426,2428,2430,2432,2434,2436],{"class":136,"line":164},[134,2415,231],{"class":147},[134,2417,859],{"class":498},[134,2419,86],{"class":147},[134,2421,445],{"class":147},[134,2423,866],{"class":151},[134,2425,450],{"class":147},[134,2427,198],{"class":147},[134,2429,873],{"class":498},[134,2431,86],{"class":147},[134,2433,878],{"class":406},[134,2435,881],{"class":157},[134,2437,884],{"class":147},[134,2439,2440,2442,2444,2446,2448,2450,2452,2454,2456,2458,2460,2462],{"class":136,"line":219},[134,2441,231],{"class":147},[134,2443,859],{"class":498},[134,2445,86],{"class":147},[134,2447,445],{"class":147},[134,2449,897],{"class":151},[134,2451,450],{"class":147},[134,2453,198],{"class":147},[134,2455,873],{"class":498},[134,2457,86],{"class":147},[134,2459,878],{"class":406},[134,2461,881],{"class":157},[134,2463,884],{"class":147},[134,2465,2466,2468,2470,2472,2474,2476,2478,2480,2482,2484,2486,2488],{"class":136,"line":268},[134,2467,231],{"class":147},[134,2469,859],{"class":498},[134,2471,86],{"class":147},[134,2473,445],{"class":147},[134,2475,924],{"class":151},[134,2477,450],{"class":147},[134,2479,198],{"class":147},[134,2481,873],{"class":498},[134,2483,86],{"class":147},[134,2485,935],{"class":406},[134,2487,881],{"class":157},[134,2489,884],{"class":147},[134,2491,2492,2494,2496,2498,2500,2502,2504,2506,2508,2510,2512,2514],{"class":136,"line":573},[134,2493,231],{"class":147},[134,2495,859],{"class":498},[134,2497,86],{"class":147},[134,2499,445],{"class":147},[134,2501,952],{"class":151},[134,2503,450],{"class":147},[134,2505,198],{"class":147},[134,2507,873],{"class":498},[134,2509,86],{"class":147},[134,2511,935],{"class":406},[134,2513,881],{"class":157},[134,2515,884],{"class":147},[134,2517,2518,2520,2522,2524,2526,2528,2530,2532,2534,2536,2538,2540],{"class":136,"line":618},[134,2519,231],{"class":147},[134,2521,859],{"class":498},[134,2523,86],{"class":147},[134,2525,445],{"class":147},[134,2527,979],{"class":151},[134,2529,450],{"class":147},[134,2531,198],{"class":147},[134,2533,873],{"class":498},[134,2535,86],{"class":147},[134,2537,990],{"class":406},[134,2539,881],{"class":157},[134,2541,884],{"class":147},[134,2543,2544,2546],{"class":136,"line":663},[134,2545,514],{"class":157},[134,2547,486],{"class":147},[12,2549,2550,2551,2554],{},"here's the portability proof — ",[42,2552,2553],{},"the same token"," on two unrelated runtimes:",[88,2556,2558],{"className":729,"code":2557,"language":731,"meta":97,"style":97},"\u002F* CSS \u002F SCSS (browser) *\u002F\n--brand-accent: #635bff;\n",[95,2559,2560,2565],{"__ignoreMap":97},[134,2561,2562],{"class":136,"line":137},[134,2563,2564],{"class":140},"\u002F* CSS \u002F SCSS (browser) *\u002F\n",[134,2566,2567,2569,2571],{"class":136,"line":144},[134,2568,1022],{"class":157},[134,2570,1025],{"class":147},[134,2572,1028],{"class":157},[88,2574,2576],{"className":1031,"code":2575,"language":1033,"meta":97,"style":97},"\u002F\u002F Flutter \u002F Dart (mobile runtime)\nstatic const Color brandAccent = Color(0xFF635BFF);\n",[95,2577,2578,2583],{"__ignoreMap":97},[134,2579,2580],{"class":136,"line":137},[134,2581,2582],{},"\u002F\u002F Flutter \u002F Dart (mobile runtime)\n",[134,2584,2585],{"class":136,"line":144},[134,2586,1045],{},[12,2588,2589,2590,2593,2594,2597,2598,332],{},"what ports is the ",[22,2591,2592],{},"approach",": want a new stack? add an emitter for the new target — ",[42,2595,2596],{},"you don't touch the source",". the whole principle: ",[22,2599,2600],{},"generate into each platform's native format",[28,2602,2603],{},[12,2604,2605,2606,2611,2612,332],{},"→ the emitters in full: ",[34,2607,2609],{"href":1067,"rel":2608},[38],[95,2610,1071],{}," · the same tokens after they reach the mobile runtime: ",[34,2613,2615],{"href":1075,"rel":2614},[38],[95,2616,1079],{},[51,2618,2620],{"id":2619},"step-3-primitives-on-tokens-not-wrappers-around-someone-elses-kit","step 3 · primitives on tokens, not wrappers around someone else's kit",[12,2622,2623,2624,2627,2628,2631],{},"the temptation is to grab a ready-made UI kit and wrap its button. but then the styles live ",[22,2625,2626],{},"inside"," someone else's code, and you can't pin rules on them. so the base primitives are ",[42,2629,2630],{},"your own",", styled with tokens only.",[12,2633,2634],{},"each component is a folder with the full set (more on that in step 4):",[88,2636,2639],{"className":2637,"code":2638,"language":93},[91],"packages\u002Fui\u002Fsrc\u002Fcomponents\u002FAppButton\u002F\n  AppButton.vue        # markup + token styles\n  AppButton.stories.ts # showcase of every variant\n  AppButton.spec.ts    # render + a11y + variants\n  index.ts\n",[95,2640,2638],{"__ignoreMap":97},[12,2642,2643,2644,2646],{},"styles reference only ",[95,2645,1109],{},", classes are BEM with a prefix, no raw values:",[88,2648,2650],{"className":1113,"code":2649,"language":1115,"meta":97,"style":97},".app-button {\n  height: var(--space-10);\n  padding: 0 var(--space-4);\n  border-radius: var(--radius-lg);\n  font-size: var(--text-md);\n  transition: background var(--dur-base) var(--ease);\n\n  &--primary {\n    \u002F\u002F BEM modifier\n    background: var(--brand-primary);\n    color: var(--brand-primary-fg);\n  }\n}\n",[95,2651,2652,2660,2674,2690,2704,2718,2742,2746,2754,2759,2773,2787,2791],{"__ignoreMap":97},[134,2653,2654,2656,2658],{"class":136,"line":137},[134,2655,332],{"class":147},[134,2657,1124],{"class":184},[134,2659,425],{"class":147},[134,2661,2662,2664,2666,2668,2670,2672],{"class":136,"line":144},[134,2663,1132],{"class":1131},[134,2665,86],{"class":147},[134,2667,1137],{"class":406},[134,2669,410],{"class":147},[134,2671,1142],{"class":157},[134,2673,1145],{"class":147},[134,2675,2676,2678,2680,2682,2684,2686,2688],{"class":136,"line":164},[134,2677,1150],{"class":1131},[134,2679,86],{"class":147},[134,2681,1156],{"class":1155},[134,2683,1137],{"class":406},[134,2685,410],{"class":147},[134,2687,1163],{"class":157},[134,2689,1145],{"class":147},[134,2691,2692,2694,2696,2698,2700,2702],{"class":136,"line":219},[134,2693,1170],{"class":1131},[134,2695,86],{"class":147},[134,2697,1137],{"class":406},[134,2699,410],{"class":147},[134,2701,1179],{"class":157},[134,2703,1145],{"class":147},[134,2705,2706,2708,2710,2712,2714,2716],{"class":136,"line":268},[134,2707,1186],{"class":1131},[134,2709,86],{"class":147},[134,2711,1137],{"class":406},[134,2713,410],{"class":147},[134,2715,1195],{"class":157},[134,2717,1145],{"class":147},[134,2719,2720,2722,2724,2726,2728,2730,2732,2734,2736,2738,2740],{"class":136,"line":573},[134,2721,1202],{"class":1131},[134,2723,86],{"class":147},[134,2725,1207],{"class":157},[134,2727,1210],{"class":406},[134,2729,410],{"class":147},[134,2731,1215],{"class":157},[134,2733,422],{"class":147},[134,2735,1137],{"class":406},[134,2737,410],{"class":147},[134,2739,1224],{"class":157},[134,2741,1145],{"class":147},[134,2743,2744],{"class":136,"line":618},[134,2745,1232],{"emptyLinePlaceholder":1231},[134,2747,2748,2750,2752],{"class":136,"line":663},[134,2749,1237],{"class":184},[134,2751,1240],{"class":157},[134,2753,161],{"class":147},[134,2755,2756],{"class":136,"line":669},[134,2757,2758],{"class":140},"    \u002F\u002F BEM modifier\n",[134,2760,2761,2763,2765,2767,2769,2771],{"class":136,"line":691},[134,2762,1252],{"class":1131},[134,2764,86],{"class":147},[134,2766,1137],{"class":406},[134,2768,410],{"class":147},[134,2770,1261],{"class":157},[134,2772,1145],{"class":147},[134,2774,2775,2777,2779,2781,2783,2785],{"class":136,"line":717},[134,2776,1268],{"class":1131},[134,2778,86],{"class":147},[134,2780,1137],{"class":406},[134,2782,410],{"class":147},[134,2784,1277],{"class":157},[134,2786,1145],{"class":147},[134,2788,2789],{"class":136,"line":1282},[134,2790,666],{"class":147},[134,2792,2793],{"class":136,"line":1287},[134,2794,271],{"class":147},[12,2796,2797,2798,2800],{},"the component API reads as intent: ",[95,2799,1295],{},", not a color. so the rebrand from step 1 passes through the entire UI without touching a single signature.",[12,2802,2803,2804,2807,2808,2811],{},"you can't fully escape someone else's kit — complex behavioral widgets (modal, dropdown, table, calendar) are cheaper to take ready-made. they're ",[42,2805,2806],{},"not banned",", they're ",[22,2809,2810],{},"isolated",": the foreign symbol is imported in exactly one place — inside its own wrapper-primitive — and only your component faces outward.",[12,2813,2814,2815,2818],{},"the principle: ",[22,2816,2817],{},"own your primitives"," — you can only check rules on code you own.",[28,2820,2821],{},[12,2822,2823,2824,332],{},"→ primitives library: ",[34,2825,2827],{"href":1322,"rel":2826},[38],[95,2828,1326],{},[51,2830,2832],{"id":2831},"step-4-teeth-machine-rules-instead-of-a-guideline","step 4 · teeth: machine rules instead of a guideline",[12,2834,2835,2836,332],{},"this is the step the whole thing was for. the rules aren't lines in a wiki — they're configs that ",[42,2837,2838],{},"fail the build",[12,2840,2841,2844],{},[42,2842,2843],{},"the linter bans hardcoding."," the actual rules:",[88,2846,2848],{"className":1345,"code":2847,"language":1347,"meta":97,"style":97},"\u002F\u002F simplified from stylelint.config.mjs\nrules: {\n  'declaration-no-important': true,        \u002F\u002F no !important\n  'color-no-hex': true,                    \u002F\u002F no #635bff — only var(--brand-*)\n  'color-named': 'never',                  \u002F\u002F no red \u002F blue\n  \u002F\u002F plus: z-index only via var(--z-*), durations via var(--dur-*),\n  \u002F\u002F       spacing ≥3px via var(--space-*); classes — BEM with an app-\u002Fhealth-\u002Fbrand- prefix\n}\n",[95,2849,2850,2855,2863,2880,2897,2918,2923,2928],{"__ignoreMap":97},[134,2851,2852],{"class":136,"line":137},[134,2853,2854],{"class":140},"\u002F\u002F simplified from stylelint.config.mjs\n",[134,2856,2857,2859,2861],{"class":136,"line":144},[134,2858,1359],{"class":184},[134,2860,86],{"class":147},[134,2862,425],{"class":147},[134,2864,2865,2867,2869,2871,2873,2875,2877],{"class":136,"line":164},[134,2866,1368],{"class":147},[134,2868,1371],{"class":151},[134,2870,450],{"class":147},[134,2872,158],{"class":498},[134,2874,1379],{"class":1378},[134,2876,198],{"class":147},[134,2878,2879],{"class":140},"        \u002F\u002F no !important\n",[134,2881,2882,2884,2886,2888,2890,2892,2894],{"class":136,"line":219},[134,2883,1368],{"class":147},[134,2885,1391],{"class":151},[134,2887,450],{"class":147},[134,2889,158],{"class":498},[134,2891,1379],{"class":1378},[134,2893,198],{"class":147},[134,2895,2896],{"class":140},"                    \u002F\u002F no #635bff — only var(--brand-*)\n",[134,2898,2899,2901,2903,2905,2907,2909,2911,2913,2915],{"class":136,"line":268},[134,2900,1368],{"class":147},[134,2902,1409],{"class":151},[134,2904,450],{"class":147},[134,2906,158],{"class":498},[134,2908,450],{"class":147},[134,2910,1418],{"class":151},[134,2912,450],{"class":147},[134,2914,198],{"class":147},[134,2916,2917],{"class":140},"                  \u002F\u002F no red \u002F blue\n",[134,2919,2920],{"class":136,"line":573},[134,2921,2922],{"class":140},"  \u002F\u002F plus: z-index only via var(--z-*), durations via var(--dur-*),\n",[134,2924,2925],{"class":136,"line":618},[134,2926,2927],{"class":140},"  \u002F\u002F       spacing ≥3px via var(--space-*); classes — BEM with an app-\u002Fhealth-\u002Fbrand- prefix\n",[134,2929,2930],{"class":136,"line":663},[134,2931,271],{"class":147},[12,2933,2934,2935,2937,2938,2940],{},"hardcode ",[95,2936,1445],{}," instead of ",[95,2939,152],{}," and lint rejects it. not an abstract agreement — a refusal.",[12,2942,2943,2946],{},[42,2944,2945],{},"the audit demands the full set per component."," a script walks the folders and fails if a component is missing its story or spec:",[88,2948,2950],{"className":389,"code":2949,"language":391,"meta":97,"style":97},"\u002F\u002F the gist of packages\u002Fui\u002Fscripts\u002Faudit-components.ts\n\u002F\u002F every component folder must contain:\n\u002F\u002F   \u003CName>.vue, \u003CName>.stories.ts, \u003CName>.spec.ts, index.ts\n\u002F\u002F otherwise process.exit(1) — \"component audit failed\".\n",[95,2951,2952,2957,2962,2966],{"__ignoreMap":97},[134,2953,2954],{"class":136,"line":137},[134,2955,2956],{"class":140},"\u002F\u002F the gist of packages\u002Fui\u002Fscripts\u002Faudit-components.ts\n",[134,2958,2959],{"class":136,"line":144},[134,2960,2961],{"class":140},"\u002F\u002F every component folder must contain:\n",[134,2963,2964],{"class":136,"line":164},[134,2965,1475],{"class":140},[134,2967,2968],{"class":136,"line":219},[134,2969,2970],{"class":140},"\u002F\u002F otherwise process.exit(1) — \"component audit failed\".\n",[12,2972,2973,2974,2976],{},"this audit actually runs in CI (the ",[95,2975,1486],{}," job), so a \"component with no showcase and no test\" physically can't reach main.",[12,2978,2979,2982,2983,1497,2985,2987],{},[42,2980,2981],{},"CI catches generated-code drift."," the artifacts are committed; CI regenerates them and diffs against the tree — a mismatch fails the PR. in this repo the drift-gate currently sits on the generated API clients (",[95,2984,1496],{},[95,2986,1500],{},"); for design tokens the same trick is the next obvious step (see \"what's next\"). for now the tokens are held by a ban on hand-editing generated files plus regenerating in place.",[12,2989,2814,2990,2993],{},[22,2991,2992],{},"rules are machine-enforced, not in a guideline",". a guideline is hope; a failing pipeline is a guarantee. and that's what turns \"the agent writes features\" from a risk into a managed process.",[28,2995,2996],{},[12,2997,2998,2999,332],{},"→ the real rules config: ",[34,3000,3002],{"href":1515,"rel":3001},[38],[95,3003,1519],{},[51,3005,3007],{"id":3006},"step-5-changes-flow-one-way-only","step 5 · changes flow one way only",[12,3009,3010],{},"tie it all into a loop. any design change runs strictly top-down:",[276,3012,3013,3020,3028,3034],{},[279,3014,3015,1534,3018,332],{},[42,3016,3017],{},"edit a token",[95,3019,1537],{},[279,3021,3022,1534,3025,3027],{},[42,3023,3024],{},"regenerate",[95,3026,1545],{},". the web \u002F Storybook \u002F mobile artifacts rebuild together.",[279,3029,3030,3033],{},[42,3031,3032],{},"update or create the component"," in the library (with story and spec).",[279,3035,3036,3039],{},[42,3037,3038],{},"consume"," it in the apps — pages stay thin: data, props, routing.",[12,3041,3042,3043,3046],{},"the rule: ",[22,3044,3045],{},"never edit \"downstream\" to fix a value \"upstream\"",". if a component renders the old color, it's using a literal instead of a token; you fix the component, you don't bend the token to match. the reverse edit is a bug, not flexibility.",[51,3048,3050],{"id":3049},"the-honest-trade-offs","the honest trade-offs",[12,3052,3053],{},"the approach isn't free — and that's fine:",[315,3055,3056,3062,3068,3077,3083],{},[279,3057,3058,3061],{},[42,3059,3060],{},"regeneration is a real step, and the artifacts are committed."," forget to rebuild and you get drift. for now a ban on hand-editing generated files holds the tokens; the CI drift-gate (already covering generated clients) doesn't extend to tokens yet — see \"what's next\".",[279,3063,3064,3067],{},[42,3065,3066],{},"a token is a contract."," renaming or deleting one = a breaking change for every consumer on every platform. only deliberately (in this repo — via an ADR).",[279,3069,3070,3073,3074,3076],{},[42,3071,3072],{},"your own primitives cost more up front"," than ",[95,3075,1595],{},"-ing a ready kit.",[279,3078,3079,3082],{},[42,3080,3081],{},"you can't fully escape someone else's kit"," — complex widgets still ride on it, just behind a wrapper.",[279,3084,3085,3088],{},[42,3086,3087],{},"a strictness tax."," \"no magic numbers\" slows down \"just throw something together fast\".",[51,3090,3092],{"id":3091},"whats-next-what-could-be-better","what's next \u002F what could be better",[12,3094,3095,3096,3099,3100,3103],{},"one direction: every next step ",[42,3097,3098],{},"narrows the gap between intent and check",", or ",[42,3101,3102],{},"shortens the loop"," from source → product.",[315,3105,3106,3112,3121,3130,3136],{},[279,3107,3108,3111],{},[42,3109,3110],{},"close the designer↔code loop"," — pull tokens straight from the design tool through the same standard (DTCG was built for exactly this), so the source stops being hand-written JSON.",[279,3113,3114,3117,3118,3120],{},[42,3115,3116],{},"make token regeneration un-forgettable"," — a CI drift-gate (as already done for generated clients via ",[95,3119,1496],{},") that regenerates tokens and fails on a mismatch. closes trade-off #1.",[279,3122,3123,3126,3127,3129],{},[42,3124,3125],{},"token aliases"," — semantic tokens reference primitives (",[95,3128,1649],{}," per DTCG), killing duplication and making rebrands even cheaper.",[279,3131,3132,3135],{},[42,3133,3134],{},"a rename codemod"," — turn the \"token = contract\" cost from manual into mechanical.",[279,3137,3138,3141],{},[42,3139,3140],{},"wider machine checks"," — a contract test for \"every role × every theme has a value\"; visual baseline snapshots per component.",[51,3143,3145],{"id":3144},"the-takeaway","the takeaway",[12,3147,3148,3149,3152],{},"the design layer here isn't a library — it's an ",[42,3150,3151],{},"invariant machine",": one neutral source, generation for any stack, and a set of linters and audits that make breaking the rules technically impossible. many design sources converge on one standard format; out of it, many targets — and the rules keep the system whole no matter who writes the code.",[12,3154,3155,3156,3160,3161,332],{},"→ see it all live: ",[34,3157,3159],{"href":36,"rel":3158},[38],"the showcase repo"," · the token-format standard — ",[34,3162,1684],{"href":114,"rel":3163},[38],[12,3165,3166],{},[22,3167,3168],{},"next in the series — the spec-first loop: how OpenAPI + codegen keep the backend and the clients from drifting apart on the contract.",[1691,3170,3171],{},"html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sqsOY, html code.shiki .sqsOY{--shiki-light:#8796B0;--shiki-default:#B2CCD6;--shiki-dark:#B2CCD6}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}",{"title":97,"searchDepth":144,"depth":164,"links":3173},[3174],{"id":1733,"depth":144,"text":1724,"children":3175},[3176,3177,3178,3179,3180,3181,3182,3183,3184,3185],{"id":1759,"depth":164,"text":1760},{"id":1784,"depth":164,"text":1785},{"id":1804,"depth":164,"text":1805},{"id":2015,"depth":164,"text":2016},{"id":2619,"depth":164,"text":2620},{"id":2831,"depth":164,"text":2832},{"id":3006,"depth":164,"text":3007},{"id":3049,"depth":164,"text":3050},{"id":3091,"depth":164,"text":3092},{"id":3144,"depth":164,"text":3145},"part 1 of the claude-driven monorepo series: how a neutral token source plus machine-checked rules make a design system's invariants impossible to break — by hand or by agent.","long read",{},"12 min",{"title":1724,"description":3186},{"label":3187,"color":1720},"li6gAZYMk7D4YMIXL5xq9RuYwQjuQsj2XPoV8G9pV_A",1781358082816]