synthesisui 0.16.421 → 0.16.423
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/claude-md.js +19 -38
- package/dist/commands/add.js +57 -94
- package/dist/commands/component.js +96 -201
- package/dist/commands/connect.js +41 -0
- package/dist/commands/doctor.js +74 -428
- package/dist/commands/generate.js +25 -4
- package/dist/commands/import.js +2 -2
- package/dist/commands/init.js +13 -10
- package/dist/commands/refit.js +13 -2
- package/dist/commands/summary.js +9 -5
- package/dist/commands/sync.js +84 -0
- package/dist/commands/template.js +81 -53
- package/dist/commands/upgrade.js +5 -3
- package/dist/commands/use.js +7 -8
- package/dist/component-codegen.js +50 -23
- package/dist/copy/connect.pt-BR.js +3 -0
- package/dist/doctor/apply-fix.js +2 -2
- package/dist/doctor/scan.js +33 -2
- package/dist/doctor/their-names.js +8 -2
- package/dist/fonts.js +29 -5
- package/dist/global-sheet.js +14 -125
- package/dist/guide.js +131 -83
- package/dist/install-marks.js +4 -4
- package/dist/local-edits.js +257 -0
- package/dist/project-facts.js +138 -32
- package/dist/recipe-css.js +249 -0
- package/dist/skills.js +5 -16
- package/dist/their-tongue.js +180 -43
- package/dist/wired-slugs.js +31 -17
- package/package.json +1 -1
- package/dist/setup-prompt.js +0 -95
- package/dist/sheet-needed.js +0 -317
- package/dist/skill-configure.js +0 -13
- package/dist/wiring-read.js +0 -212
package/dist/guide.js
CHANGED
|
@@ -293,8 +293,49 @@ toolsReachable = false,
|
|
|
293
293
|
* essa porta; deixá-la entreaberta um nível acima seria vigiá-la em vez de fechá-la
|
|
294
294
|
* (CLAUDE.md, Parte I, seção 6).
|
|
295
295
|
*/
|
|
296
|
-
fontFacts
|
|
296
|
+
fontFacts,
|
|
297
|
+
/**
|
|
298
|
+
* O VOCABULÁRIO DESTE REPOSITÓRIO - `tongueFromArtifacts`, ou `null` quando não há folha para ler.
|
|
299
|
+
*
|
|
300
|
+
* O QUE ISTO CONSERTA, e é a razão de existir desta etapa: este arquivo é o que o agente DELE lê
|
|
301
|
+
* antes de escrever, e ele ensinava a nossa grafia. *"Color: `var(--ds-color-semantic-<role>)`"*
|
|
302
|
+
* é uma instrução para escrever a NOSSA variável no código dele - e ela só resolve com a nossa
|
|
303
|
+
* folha carregada, que é o que `INV-GERAL-13` proíbe. Um guia que ensina isso produz a violação
|
|
304
|
+
* sem que ninguém decida nada: o agente obedece.
|
|
305
|
+
*
|
|
306
|
+
* Com o vocabulário em mão, cada linha abaixo diz o nome que o código DELE dá àquele valor - ou o
|
|
307
|
+
* valor, quando o código dele não nomeia nenhum.
|
|
308
|
+
*
|
|
309
|
+
* OBRIGATÓRIO, pelo mesmo argumento de `fontFacts` logo acima: um argumento opcional aqui é um
|
|
310
|
+
* argumento que um chamador novo esquece, e o esquecimento devolve a nossa grafia ao arquivo que
|
|
311
|
+
* mais a propaga.
|
|
312
|
+
*/
|
|
313
|
+
tongue) {
|
|
297
314
|
const { document: doc, slug, name, version } = payload;
|
|
315
|
+
/**
|
|
316
|
+
* COMO ISTO SE ESCREVE NO CÓDIGO DELE - a mesma resposta que a materialização usa.
|
|
317
|
+
*
|
|
318
|
+
* `var(--nome-dele)` quando o repositório dele nomeia aquele valor; o valor literal quando não.
|
|
319
|
+
* Sem vocabulário (projeto que nunca buildou, folha ausente) a resposta é `null`, e quem chama
|
|
320
|
+
* DIZ isso em vez de imprimir uma grafia que ninguém pode escrever.
|
|
321
|
+
*/
|
|
322
|
+
const saysAs = (ours) => {
|
|
323
|
+
if (!tongue)
|
|
324
|
+
return null;
|
|
325
|
+
const theirs = tongue.names.get(ours);
|
|
326
|
+
if (theirs)
|
|
327
|
+
return `var(${theirs})`;
|
|
328
|
+
return tongue.values.get(ours) ?? null;
|
|
329
|
+
};
|
|
330
|
+
/** `spacing` -> `md → var(--gap-md)`, para cada degrau que o vocabulário alcança. */
|
|
331
|
+
const family = (prefix, keys) => {
|
|
332
|
+
const said = keys
|
|
333
|
+
.map((k) => ({ k, as: saysAs(`--ds-${prefix}-${kebab(k)}`) }))
|
|
334
|
+
.filter((x) => x.as !== null);
|
|
335
|
+
return said.length > 0
|
|
336
|
+
? said.map((x) => `\`${x.k}\` → \`${x.as}\``).join(", ")
|
|
337
|
+
: list(keys);
|
|
338
|
+
};
|
|
298
339
|
const { meta, foundations, motion, components } = doc;
|
|
299
340
|
const semanticRoles = Object.keys(foundations.color.semantic);
|
|
300
341
|
/**
|
|
@@ -339,8 +380,6 @@ fontFacts) {
|
|
|
339
380
|
*/
|
|
340
381
|
const fontPlan = nextFontSnippet({
|
|
341
382
|
families: foundations.typography.families,
|
|
342
|
-
slug,
|
|
343
|
-
seam: payload.fontSeam,
|
|
344
383
|
facts: fontFacts,
|
|
345
384
|
/** Os pesos que ELE declara - ver `declaredWeights`. O guia é o arquivo que o agente dele cola. */
|
|
346
385
|
declaredWeights: foundations.typography.weights,
|
|
@@ -548,7 +587,10 @@ It writes a **deterministic scaffold**: the page uses this DS's \`.ds-*\` recipe
|
|
|
548
587
|
path-classes, paired with a co-located scoped CSS (Next target) that carries the **responsive** media
|
|
549
588
|
queries and the **CSS-only hamburger** - so the page is mobile-ready out of the box. **Refine it in
|
|
550
589
|
place** - wire real data, split into components, swap the chart/icon/media placeholders - but keep the
|
|
551
|
-
|
|
590
|
+
wrapper and the \`.ds-*\` / layout classes: the stylesheet next to the page carries the rules for the
|
|
591
|
+
ones it wears, scoped to that wrapper - the command says how many came along, so the page stands on
|
|
592
|
+
its own with nothing imported. A \`.ds-*\` you ADD while refining has no rule there; bring that piece
|
|
593
|
+
in with \`synthesisui component\` instead. Run
|
|
552
594
|
\`synthesisui init\` once to set the target (next/general) and the output folder.
|
|
553
595
|
|
|
554
596
|
## Single components as YOUR code
|
|
@@ -560,8 +602,8 @@ synthesisui component ${slug} button
|
|
|
560
602
|
It writes \`<componentsDir>/button/\` with \`button.tsx\` (+ colocated \`button.css\` in the default
|
|
561
603
|
\`styles: "css"\` flavor, or Tailwind utilities inline with \`styles: "tailwind"\` - set once via
|
|
562
604
|
\`synthesisui init --styles tailwind\`). Import and render: \`import { Button } from "components/button"\`.
|
|
563
|
-
The boundary:
|
|
564
|
-
|
|
605
|
+
The boundary: everything a component owns lives in its own folder, and every value in it is a name
|
|
606
|
+
YOUR code declares or the value itself - there is nothing global to set up.
|
|
565
607
|
|
|
566
608
|
**A component's conditional classes are decided at the call site, not in the stylesheet.** A recipe
|
|
567
609
|
carries the look of a state as a rule keyed on a data attribute (\`[data-size="lg"]\`), and the caller
|
|
@@ -623,80 +665,73 @@ ${meta.narrative}
|
|
|
623
665
|
${rulesNote}
|
|
624
666
|
## How to apply
|
|
625
667
|
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
@import "../_synthesisui/ds/${slug}/tokens.css";
|
|
630
|
-
\`\`\`
|
|
631
|
-
(drop the \`../\` if your global CSS sits at the project root.)${hasTailwind
|
|
632
|
-
? `\n **Using Tailwind (this project's default)? You also need \`theme.css\` right after \`tokens.css\` -\n without it the DS-backed utilities (\`bg-primary\`, \`p-md\`…) aren't generated and the UI renders\n unstyled. See "Styling with Tailwind v4" below for the full import block.**`
|
|
633
|
-
: ""}
|
|
668
|
+
**Nothing here is imported into your app.** \`_synthesisui/ds/${slug}/\` is the reference this guide,
|
|
669
|
+
the check, the doctor and the MCP server read to interpret your design - your code never points at
|
|
670
|
+
it, and nothing we write into this repository needs it at runtime.
|
|
634
671
|
|
|
635
|
-
|
|
636
|
-
\`\`\`html
|
|
637
|
-
<div data-ds="${slug}">…your UI here…</div>
|
|
638
|
-
\`\`\`
|
|
639
|
-
All \`--ds-*\` custom properties and \`.ds-*\` classes only apply inside that scope.
|
|
640
|
-
Applying \`data-ds="${slug}"\` at the app root (e.g. \`<body>\` or the root layout)
|
|
641
|
-
is the simplest choice - the whole app then wears the system.
|
|
642
|
-
${hasAlt
|
|
643
|
-
? `
|
|
644
|
-
3. Light/dark: an ancestor with \`data-scheme="${altScheme}"\` switches the neutral roles to the opposite mode.
|
|
645
|
-
\`\`\`tsx
|
|
646
|
-
<div data-scheme="${altScheme}"><div data-ds="${slug}">…</div></div>
|
|
672
|
+
1. Ask for a component and it arrives as YOUR code:
|
|
647
673
|
\`\`\`
|
|
648
|
-
|
|
649
|
-
\`\`\`tsx
|
|
650
|
-
root.toggleAttribute("data-scheme"); // present = ${altScheme}, absent = ${meta.scheme}
|
|
674
|
+
npx synthesisui component ${slug} <name>
|
|
651
675
|
\`\`\`
|
|
652
|
-
|
|
676
|
+
It lands in your components directory speaking the names your own code declares - or carrying the
|
|
677
|
+
value itself, where your code names none. No import, no scope attribute, nothing at runtime.
|
|
678
|
+
|
|
679
|
+
2. Writing something by hand? Use the vocabulary below. Every name in it is a name **your own code
|
|
680
|
+
declares**, written exactly as you would write it.
|
|
681
|
+
${hasAlt
|
|
682
|
+
? `\n3. Light/dark: this system's roles have two readings, and a component that arrives from
|
|
683
|
+
\`component\` carries both. Which one shows is your app's own scheme switch - we do not add one.\n`
|
|
653
684
|
: ""}${fontsSection}${depsSection}${hasTailwind
|
|
654
685
|
? `
|
|
655
686
|
## Styling with Tailwind v4 (preferred in this project)
|
|
656
687
|
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
@import "./_synthesisui/ds/${slug}/tokens.css";
|
|
661
|
-
@import "./_synthesisui/ds/${slug}/theme.css";
|
|
662
|
-
\`\`\`
|
|
663
|
-
This maps the DS tokens onto Tailwind's theme, so inside \`[data-ds="${slug}"]\` you get utilities
|
|
664
|
-
backed by the design system: \`bg-*\`/\`text-*\`/\`border-*\` (semantic colors), \`p-*\`/\`m-*\`/\`gap-*\`
|
|
665
|
-
(spacing), \`rounded-*\`, \`shadow-*\`, \`font-*\` (families **and** weights), \`text-*\` (type scale), \`ease-*\`${seriesKeys.length > 0
|
|
666
|
-
? `, \`bg-series-*\`/\`text-series-*\`/\`fill-series-*\` (data-viz series)`
|
|
667
|
-
: ""}.
|
|
688
|
+
Your own \`@theme\` is what backs the utilities, and it is the one that decides. Where it declares a
|
|
689
|
+
name, the utility already paints your value and it is the most readable code you can write; where it
|
|
690
|
+
does not, write the value.
|
|
668
691
|
|
|
669
|
-
**Prefer
|
|
670
|
-
|
|
692
|
+
**Prefer utilities for layout and new composition** - they are this project's idiom and read far
|
|
693
|
+
better than inline \`style\`.
|
|
671
694
|
|
|
672
695
|
\`\`\`tsx
|
|
673
|
-
//
|
|
674
|
-
<main className="
|
|
675
|
-
<button className="
|
|
696
|
+
// preferred - your project's own utilities
|
|
697
|
+
<main className="p-2xl flex flex-col gap-md">
|
|
698
|
+
<button className="rounded-md px-md py-2xs">Save</button>
|
|
676
699
|
</main>
|
|
700
|
+
\`\`\`
|
|
701
|
+
|
|
702
|
+
To give a utility the system's value, declare the name in **your own** \`@theme\` - it is your
|
|
703
|
+
stylesheet, your namespace, and nothing of ours is involved:
|
|
677
704
|
|
|
678
|
-
|
|
679
|
-
|
|
705
|
+
\`\`\`css
|
|
706
|
+
@theme {
|
|
707
|
+
--radius-md: <the value you want rounded-md to paint>;
|
|
708
|
+
}
|
|
680
709
|
\`\`\`
|
|
681
710
|
|
|
711
|
+
Do that and \`synthesisui component\` starts writing \`rounded-md\` instead of the raw value, because
|
|
712
|
+
the name now exists in your project.
|
|
713
|
+
|
|
682
714
|
---
|
|
683
715
|
`
|
|
684
716
|
: ""}
|
|
685
|
-
This is **v${version}**. The
|
|
686
|
-
\`tokens.css\`/\`theme.css\` re-exports, plus \`.lock\`) always point at the active version - import
|
|
687
|
-
those, not the versioned ones. The pinned files for this version - ${artifactList},
|
|
717
|
+
This is **v${version}**. The pinned files for this version - ${artifactList},
|
|
688
718
|
\`design-system.json\` (canonical source of truth), \`GUIDE.md\` (this file) - live in
|
|
689
|
-
\`_synthesisui/ds/${slug}/v${version}
|
|
719
|
+
\`_synthesisui/ds/${slug}/v${version}/\`, and they are OURS to read: the check, the doctor and the MCP
|
|
720
|
+
server use them to interpret your design. Your app imports none of them.
|
|
690
721
|
|
|
691
722
|
---
|
|
692
723
|
${pagesSection}
|
|
693
724
|
## Building with the system
|
|
694
725
|
|
|
695
726
|
**This system is for building real product UI** - pages, layouts, dashboards, whole flows.
|
|
696
|
-
|
|
697
|
-
actual screens. There is **no "samples only" rule**: build the real
|
|
698
|
-
\`app/synthesisui-samples/<component>/\` page is a fine *optional* scratch space to eyeball a
|
|
699
|
-
component, but it is never required.
|
|
727
|
+
Bring the recipes in as YOUR code (\`npx synthesisui component ${slug} <name>\`) and compose them with
|
|
728
|
+
your own utilities to assemble actual screens. There is **no "samples only" rule**: build the real
|
|
729
|
+
app. An \`app/synthesisui-samples/<component>/\` page is a fine *optional* scratch space to eyeball a
|
|
730
|
+
single component, but it is never required.
|
|
731
|
+
|
|
732
|
+
The \`.ds-*\` class names below describe the recipe's SHAPE - which parts it has and which
|
|
733
|
+
\`data-*\` each one takes. They are what the materialized component wears, and the stylesheet that
|
|
734
|
+
declares them comes with it.
|
|
700
735
|
|
|
701
736
|
### Layout & composition
|
|
702
737
|
The system defines the scale; these are sensible defaults for spending it:
|
|
@@ -728,10 +763,9 @@ part classes and their \`data-*\` are listed per component below. Example - a ta
|
|
|
728
763
|
`
|
|
729
764
|
: ""}
|
|
730
765
|
### Overlays & portals
|
|
731
|
-
Dialogs, menus and toasts are often rendered through a portal at the end of \`<body>\` -
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
app root so everything (portals included) inherits it. Behavior (open/close, focus trap, positioning,
|
|
766
|
+
Dialogs, menus and toasts are often rendered through a portal at the end of \`<body>\` - outside
|
|
767
|
+
the tree they were written in. A component brought in by \`synthesisui component\` carries its own
|
|
768
|
+
stylesheet and works anywhere, portal included. Behavior (open/close, focus trap, positioning,
|
|
735
769
|
keyboard) is yours to wire - the system ships the **looks**, not the JavaScript.
|
|
736
770
|
|
|
737
771
|
### Interactive recipes - the behavior contract
|
|
@@ -756,34 +790,47 @@ styles it, you make it work.
|
|
|
756
790
|
## Rules (follow them when creating components)
|
|
757
791
|
${hasTailwind
|
|
758
792
|
? `
|
|
759
|
-
- **Styling mechanism:** prefer Tailwind utilities
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
793
|
+
- **Styling mechanism:** prefer your project's own Tailwind utilities for layout and new
|
|
794
|
+
composition, and bring the recipes in as code (\`synthesisui component ${slug} <name>\`) for the
|
|
795
|
+
components this system already covers. Where no utility fits, write the name from the vocabulary
|
|
796
|
+
below - it is a name your own code declares.`
|
|
763
797
|
: ""}
|
|
764
|
-
- **Always use
|
|
765
|
-
|
|
798
|
+
- **Always use the system's vocabulary**, never raw values picked by eye. Every name below is a
|
|
799
|
+
name **your own code declares** - written exactly as you would write it. Nothing here comes from
|
|
800
|
+
a stylesheet of ours, and nothing needs one.
|
|
801
|
+
Color${hasTailwind ? " (utility: `bg-<role>`/`text-<role>`)" : ""}: ${semanticRoles
|
|
802
|
+
.map((r) => {
|
|
766
803
|
const theirs = theirRole(r);
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
804
|
+
/** O papel com o nome que ELE dá a ele - e o parêntese só sai quando os dois diferem. */
|
|
805
|
+
const named = theirs === r ? `\`${r}\`` : `\`${r} (${theirs} in your code)\``;
|
|
806
|
+
const as = saysAs(`--ds-color-semantic-${kebab(r)}`);
|
|
807
|
+
return as ? `${named} → \`${as}\`` : named;
|
|
808
|
+
})
|
|
809
|
+
.join(", ")}.${namedColours.length > 0
|
|
810
|
+
? `\n- **Yours by name**: ${namedColours.length} colour${namedColours.length === 1 ? "" : "s"} your code names by PURPOSE rather than by step, so no scale could hold ${namedColours.length === 1 ? "it" : "them"} - ${family("color", namedColours.slice(0, 8))}${namedColours.length > 8 ? ", …" : ""}. These are yours: reach for them when the purpose matches, and prefer a semantic role when it does not.`
|
|
772
811
|
: ""}${seriesKeys.length > 0
|
|
773
|
-
? `\n- Data-viz
|
|
812
|
+
? `\n- Data-viz${hasTailwind ? " (utility: `bg-series-<n>`/`text-series-<n>`/`fill-series-<n>`)" : ""}: categorical chart/series colors, ${seriesKeys.length} of them - ${family("color-series", seriesKeys)}. Use them in order for multi-series charts; they re-paint with the system.`
|
|
774
813
|
: ""}
|
|
775
|
-
- Spacing →
|
|
776
|
-
- Radius →
|
|
777
|
-
- Shadow →
|
|
814
|
+
- Spacing → ${family("spacing", Object.keys(foundations.spacing))}.
|
|
815
|
+
- Radius → ${family("radius", Object.keys(foundations.radius))}.
|
|
816
|
+
- Shadow → ${family("shadow", Object.keys(foundations.shadow))}.
|
|
778
817
|
- Typography: families ${familySlots(foundations.typography.families)
|
|
779
|
-
.map((slot) =>
|
|
818
|
+
.map((slot) => {
|
|
819
|
+
const as = saysAs(`${FAMILY_SEAM_PREFIX}${kebab(slot)}`);
|
|
820
|
+
const face = foundations.typography.families[slot];
|
|
821
|
+
return as ? `\`${slot}\` → \`${as}\` (${face})` : `\`${slot}\` (${face})`;
|
|
822
|
+
})
|
|
780
823
|
.join(", ")};
|
|
781
824
|
weights${hasTailwind ? " (utility: `font-<key>`)" : ""}: ${list(weights)};
|
|
782
|
-
scale
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
825
|
+
scale${hasTailwind ? " (utility: `text-<key>`)" : ""}: ${Object.keys(foundations.typography.scale)
|
|
826
|
+
.map((k) => {
|
|
827
|
+
const as = saysAs(`--ds-typography-scale-${kebab(k)}-font-size`);
|
|
828
|
+
return as ? `\`${k}\` → \`${as}\`` : `\`${k}\``;
|
|
829
|
+
})
|
|
830
|
+
.join(", ")}.
|
|
831
|
+
- Motion: durations ${family("motion-durations", Object.keys(motion.durations))}
|
|
832
|
+
and easings ${family("motion-easings", Object.keys(motion.easings))}. Use them on
|
|
833
|
+
\`transition\` so timing stays on-brand. For ANIMATION, this system ships a named vocabulary - see
|
|
787
834
|
**Motion vocabulary** below; never hand-roll \`@keyframes\` or raw durations.
|
|
788
835
|
- When **creating a new component** the DS does not cover yet: compose it from these semantic
|
|
789
836
|
tokens to inherit the system's identity; do not invent colors/measures outside the scale.
|
|
@@ -817,7 +864,8 @@ tell the person what you chose from the vocabulary instead.`
|
|
|
817
864
|
|
|
818
865
|
## Ready-made components
|
|
819
866
|
|
|
820
|
-
Each recipe
|
|
867
|
+
Each recipe has a \`.ds-<name>\` class - the SHAPE it wears when \`synthesisui component\` writes it
|
|
868
|
+
into your project, with the stylesheet that declares it alongside. Variants are
|
|
821
869
|
\`data-<axis>="<option>"\` attributes; states (hover/focus/active/disabled) ship in the CSS;
|
|
822
870
|
multi-part components expose \`.ds-<name>-<part>\` classes (listed under each).
|
|
823
871
|
|
|
@@ -832,7 +880,7 @@ A small gamification library the AI advisor (\`synthesisui advise\`) can propose
|
|
|
832
880
|
\`.ds-<name>\` recipe shape as the components above, token-only so they wear the system. Use them
|
|
833
881
|
**only where they fit the product** (progress, retention, recognition); they're a library to compose
|
|
834
882
|
from, not a default - and lean against over-gamifying a serious B2B product. Each is a \`.ds-<name>\`
|
|
835
|
-
|
|
883
|
+
recipe; multi-part ones expose \`.ds-<name>-<part>\`.
|
|
836
884
|
|
|
837
885
|
${blockLines.join("\n\n")}
|
|
838
886
|
`
|
package/dist/install-marks.js
CHANGED
|
@@ -235,7 +235,7 @@
|
|
|
235
235
|
* chama `wireAgent`, então rodar o comando é o caminho de volta - e ele só alarga a string que era
|
|
236
236
|
* nossa, nunca um filtro que uma pessoa escreveu.
|
|
237
237
|
*/
|
|
238
|
-
export const MATERIALISER_SINCE = "0.16.
|
|
238
|
+
export const MATERIALISER_SINCE = "0.16.423";
|
|
239
239
|
/**
|
|
240
240
|
* A ÚLTIMA VERSÃO EM QUE O QUE O HOOK RODA MUDOU.
|
|
241
241
|
*
|
|
@@ -343,8 +343,8 @@ export const MATERIALISER_SINCE = "0.16.421";
|
|
|
343
343
|
* A frase viaja ao lado da versão porque as duas são uma coisa só: quem move a marca troca a
|
|
344
344
|
* explicação no mesmo lugar, em vez de deixar o CI citando a causa da marca anterior.
|
|
345
345
|
*/
|
|
346
|
-
export const COUNTED_DIFFERENTLY_SINCE = "0.16.
|
|
347
|
-
export const COUNTED_DIFFERENTLY = "this run counts
|
|
346
|
+
export const COUNTED_DIFFERENTLY_SINCE = "0.16.422";
|
|
347
|
+
export const COUNTED_DIFFERENTLY = "this run counts a value as named only when YOUR code names it - that version also counted the ones only the system named";
|
|
348
348
|
/**
|
|
349
349
|
* 0.16.408 -> 0.16.413 em 10/09: o hook passa a DIZER o valor que nada nomeia. Ele silenciava toda
|
|
350
350
|
* deriva sem token de destino - a decisão estava escrita como "não vale interromper, porque o único
|
|
@@ -366,7 +366,7 @@ export const COUNTED_DIFFERENTLY = "this run counts the utilities of your own th
|
|
|
366
366
|
* utility, um arquivo escrito inteiro no vocabulário do sistema recebia zero. A contagem em si é a
|
|
367
367
|
* outra marca - ver `COUNTED_DIFFERENTLY_SINCE`, que sobe no mesmo diff.
|
|
368
368
|
*/
|
|
369
|
-
export const CHECKER_SINCE = "0.16.
|
|
369
|
+
export const CHECKER_SINCE = "0.16.422";
|
|
370
370
|
/**
|
|
371
371
|
* A ÚLTIMA VERSÃO EM QUE OS LEITORES PASSARAM A PRODUZIR UM CENSO DIFERENTE.
|
|
372
372
|
*
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
/** A forma mínima que faz de um objeto um design system - abaixo disto não há o que comparar. */
|
|
4
|
+
function shapeOf(value) {
|
|
5
|
+
if (!value || typeof value !== "object")
|
|
6
|
+
return "it is not an object";
|
|
7
|
+
const doc = value;
|
|
8
|
+
if (!doc.meta || typeof doc.meta !== "object")
|
|
9
|
+
return "it has no `meta` - a design system always carries its name and slug";
|
|
10
|
+
if (!doc.foundations || typeof doc.foundations !== "object")
|
|
11
|
+
return "it has no `foundations` - a design system always carries its tokens";
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
async function readDocument(path) {
|
|
15
|
+
let raw;
|
|
16
|
+
try {
|
|
17
|
+
raw = await readFile(path, "utf8");
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
let parsed;
|
|
23
|
+
try {
|
|
24
|
+
parsed = JSON.parse(raw);
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
return {
|
|
28
|
+
ok: false,
|
|
29
|
+
why: error instanceof Error ? error.message : "it is not valid JSON",
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
const wrong = shapeOf(parsed);
|
|
33
|
+
if (wrong)
|
|
34
|
+
return { ok: false, why: wrong };
|
|
35
|
+
return { ok: true, document: parsed };
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* QUAL VERSÃO ESTÁ MATERIALIZADA NESTE REPOSITÓRIO - a que o `.lock` registra.
|
|
39
|
+
*
|
|
40
|
+
* Ler a pasta `v*` mais alta seria adivinhar: um `add --version N` deixa as duas no disco, e o
|
|
41
|
+
* `.lock` é o que diz qual delas o repositório está usando. `null` quando não há install.
|
|
42
|
+
*/
|
|
43
|
+
export async function installedVersion(root, slug) {
|
|
44
|
+
try {
|
|
45
|
+
const raw = await readFile(join(root, "_synthesisui", "ds", slug, ".lock"), "utf8");
|
|
46
|
+
const lock = JSON.parse(raw);
|
|
47
|
+
return typeof lock.version === "number" ? lock.version : null;
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/** O par que a comparação precisa: o que foi instalado, e o que está no disco agora. */
|
|
54
|
+
export async function readInstalledPair(root, slug, version) {
|
|
55
|
+
const dir = join(root, "_synthesisui", "ds", slug, `v${version}`);
|
|
56
|
+
const local = await readDocument(join(dir, "design-system.json"));
|
|
57
|
+
if (local === null)
|
|
58
|
+
return { kind: "not-installed" };
|
|
59
|
+
if (!local.ok)
|
|
60
|
+
return {
|
|
61
|
+
kind: "unreadable",
|
|
62
|
+
where: join(dir, "design-system.json"),
|
|
63
|
+
why: local.why,
|
|
64
|
+
};
|
|
65
|
+
const installed = await readDocument(join(dir, ".installed.json"));
|
|
66
|
+
if (installed === null)
|
|
67
|
+
return {
|
|
68
|
+
kind: "no-baseline",
|
|
69
|
+
/**
|
|
70
|
+
* O CAMINHO DE SAÍDA AVISA DO RISCO ANTES DO COMANDO.
|
|
71
|
+
*
|
|
72
|
+
* `add` reescreve `design-system.json` sem comparar nada. Mandar rodá-lo sem dizer isso
|
|
73
|
+
* apagaria, em silêncio, exatamente a edição que este recurso existe para preservar - e a
|
|
74
|
+
* pessoa teria seguido o passo que a plataforma recomendou.
|
|
75
|
+
*/
|
|
76
|
+
why: `this install predates the file that tracks your edits, and \`add\` rewrites design-system.json. If you already edited it, copy it first (cp _synthesisui/ds/${slug}/v<n>/design-system.json /tmp/${slug}-before-add.json), then run \`npx synthesisui@latest add ${slug}\` once - every edit made after that comes up with the next sync.`,
|
|
77
|
+
};
|
|
78
|
+
if (!installed.ok)
|
|
79
|
+
return {
|
|
80
|
+
kind: "unreadable",
|
|
81
|
+
where: join(dir, ".installed.json"),
|
|
82
|
+
why: installed.why,
|
|
83
|
+
};
|
|
84
|
+
return { kind: "pair", installed: installed.document, local: local.document };
|
|
85
|
+
}
|
|
86
|
+
/** Achata fundação e motion em `caminho → valor`, na mesma gramática do documento. */
|
|
87
|
+
function flatten(value, prefix, into) {
|
|
88
|
+
if (!value || typeof value !== "object")
|
|
89
|
+
return;
|
|
90
|
+
for (const [key, inner] of Object.entries(value)) {
|
|
91
|
+
const path = prefix ? `${prefix}.${key}` : key;
|
|
92
|
+
if (typeof inner === "string" || typeof inner === "number")
|
|
93
|
+
into.set(path, String(inner));
|
|
94
|
+
else
|
|
95
|
+
flatten(inner, path, into);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* O QUE NÃO VIAJA, DITO EM VOZ ALTA - lei 8, e a lista é curta de propósito.
|
|
100
|
+
*
|
|
101
|
+
* `meta` é a identidade do sistema (nome, slug, tagline): mudá-la é ato dela na plataforma, não
|
|
102
|
+
* do agente dele. `philosophy` e `analyses` são texto que a plataforma escreve e nenhum prompt
|
|
103
|
+
* pede para editar. Os três ficam fora porque não são decisão do código dele - e é por isso que
|
|
104
|
+
* a ausência deles aqui é uma escolha, e não um esquecimento.
|
|
105
|
+
*/
|
|
106
|
+
export const NOT_FROM_HIS_FILES = ["meta", "philosophy", "analyses"];
|
|
107
|
+
const RECIPE_MAPS = [
|
|
108
|
+
["components", "component"],
|
|
109
|
+
["blocks", "block"],
|
|
110
|
+
["layouts", "layout"],
|
|
111
|
+
["charts", "chart"],
|
|
112
|
+
];
|
|
113
|
+
/**
|
|
114
|
+
* A PROCEDÊNCIA NÃO É UM TOKEN, e o mesmo defeito já custou uma sessão do outro lado.
|
|
115
|
+
*
|
|
116
|
+
* `foundations.source` guarda quem escreveu cada caminho. Sem esta linha, um carimbo mudado por
|
|
117
|
+
* um comando nosso apareceria como "o agente dele trocou um valor de design".
|
|
118
|
+
*/
|
|
119
|
+
function tokensOf(doc) {
|
|
120
|
+
const out = new Map();
|
|
121
|
+
const { source: _source, ...rest } = doc.foundations;
|
|
122
|
+
flatten(rest, "", out);
|
|
123
|
+
/**
|
|
124
|
+
* E TODO EIXO DE RAIZ QUE NÃO É RECEITA, varrido pelo que o documento TEM - e não por uma
|
|
125
|
+
* lista escrita à mão.
|
|
126
|
+
*
|
|
127
|
+
* A primeira escrita enumerava `foundations` e `motion`, e deixava de fora `icons` e
|
|
128
|
+
* `globals` - este último carrega a folha de projeto inteira, 107 declarações num cliente
|
|
129
|
+
* real. O agente editava, o `sync` dizia "nothing new", e a alteração morria no disco.
|
|
130
|
+
* Varrer o que existe faz um eixo NOVO do contrato entrar sozinho, em vez de esperar alguém
|
|
131
|
+
* lembrar.
|
|
132
|
+
*/
|
|
133
|
+
const root = doc;
|
|
134
|
+
const recipes = new Set(RECIPE_MAPS.map(([key]) => key));
|
|
135
|
+
for (const key of Object.keys(root)) {
|
|
136
|
+
if (key === "foundations")
|
|
137
|
+
continue;
|
|
138
|
+
if (recipes.has(key))
|
|
139
|
+
continue;
|
|
140
|
+
if (NOT_FROM_HIS_FILES.includes(key))
|
|
141
|
+
continue;
|
|
142
|
+
flatten(root[key], key, out);
|
|
143
|
+
}
|
|
144
|
+
return out;
|
|
145
|
+
}
|
|
146
|
+
function fileOf(recipe) {
|
|
147
|
+
return recipe?.source?.file;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* O QUE MUDOU ENTRE O QUE FOI INSTALADO E O QUE ESTÁ NO DISCO.
|
|
151
|
+
*
|
|
152
|
+
* Puro sobre os dois documentos. Uma receita conta como UMA alteração, e não uma por declaração:
|
|
153
|
+
* o que o cliente lê no terminal é "o agente mexeu no Button", e a lista de declarações mora no
|
|
154
|
+
* documento que sobe junto.
|
|
155
|
+
*/
|
|
156
|
+
export function localEdits(installed, local) {
|
|
157
|
+
const out = [];
|
|
158
|
+
const was = tokensOf(installed);
|
|
159
|
+
const now = tokensOf(local);
|
|
160
|
+
for (const path of [...new Set([...was.keys(), ...now.keys()])].sort()) {
|
|
161
|
+
const from = was.get(path);
|
|
162
|
+
const to = now.get(path);
|
|
163
|
+
if (from === to)
|
|
164
|
+
continue;
|
|
165
|
+
out.push({
|
|
166
|
+
what: path,
|
|
167
|
+
of: "token",
|
|
168
|
+
kind: from === undefined ? "added" : to === undefined ? "removed" : "changed",
|
|
169
|
+
...(from !== undefined ? { from } : {}),
|
|
170
|
+
...(to !== undefined ? { to } : {}),
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
for (const [key, of_] of RECIPE_MAPS) {
|
|
174
|
+
const before = installed[key];
|
|
175
|
+
const after = local[key];
|
|
176
|
+
const names = [
|
|
177
|
+
...new Set([...Object.keys(before ?? {}), ...Object.keys(after ?? {})]),
|
|
178
|
+
].sort();
|
|
179
|
+
for (const name of names) {
|
|
180
|
+
const from = before?.[name];
|
|
181
|
+
const to = after?.[name];
|
|
182
|
+
if (from === undefined && to === undefined)
|
|
183
|
+
continue;
|
|
184
|
+
if (from !== undefined && to !== undefined) {
|
|
185
|
+
if (JSON.stringify(from) === JSON.stringify(to))
|
|
186
|
+
continue;
|
|
187
|
+
const file = fileOf(to) ?? fileOf(from);
|
|
188
|
+
out.push({
|
|
189
|
+
what: name,
|
|
190
|
+
of: of_,
|
|
191
|
+
kind: "changed",
|
|
192
|
+
...(file ? { file } : {}),
|
|
193
|
+
});
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
const file = fileOf(to ?? from);
|
|
197
|
+
out.push({
|
|
198
|
+
what: name,
|
|
199
|
+
of: of_,
|
|
200
|
+
kind: from === undefined ? "added" : "removed",
|
|
201
|
+
...(file ? { file } : {}),
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return out;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* QUANTAS LINHAS O TERMINAL NOMEIA ANTES DE DIZER QUANTAS SOBRARAM.
|
|
209
|
+
*
|
|
210
|
+
* Doze é o que cabe numa tela sem rolagem depois do resto da saída do `sync`, e o que não couber
|
|
211
|
+
* é DITO - uma lista cortada em silêncio faz doze alterações parecerem doze quando são quarenta.
|
|
212
|
+
*/
|
|
213
|
+
const NAMED = 12;
|
|
214
|
+
/**
|
|
215
|
+
* O QUE O `sync` DIZ SOBRE O QUE ELE ENCONTROU NOS ARQUIVOS - nunca um "ok" mudo, e nunca uma
|
|
216
|
+
* afirmação de sucesso antes de o servidor responder.
|
|
217
|
+
*
|
|
218
|
+
* A primeira escrita dizia *"N changes from your files WENT UP"*, e ela era montada antes do
|
|
219
|
+
* envio: quando o servidor recusava, a saída afirmava sucesso numa linha e fracasso na seguinte,
|
|
220
|
+
* na mesma tela. Quem lê rápido carrega a primeira. Esta frase relata o que foi ENCONTRADO; quem
|
|
221
|
+
* diz que chegou é `landedLine`, depois da resposta.
|
|
222
|
+
*/
|
|
223
|
+
export function sentLine(edits, slug) {
|
|
224
|
+
if (edits.length === 0)
|
|
225
|
+
return `nothing new in ${slug} - the design system on this machine matches what the platform has`;
|
|
226
|
+
const head = `${edits.length} change${edits.length === 1 ? "" : "s"} found in your files, from ${slug}:`;
|
|
227
|
+
const lines = edits.slice(0, NAMED).map((edit) => {
|
|
228
|
+
const where = edit.file ? ` · ${edit.file}` : "";
|
|
229
|
+
const move = edit.from !== undefined && edit.to !== undefined
|
|
230
|
+
? ` ${edit.from} → ${edit.to}`
|
|
231
|
+
: edit.kind === "added"
|
|
232
|
+
? " (new)"
|
|
233
|
+
: edit.kind === "removed"
|
|
234
|
+
? " (gone)"
|
|
235
|
+
: "";
|
|
236
|
+
return ` ${edit.what}${move}${where}`;
|
|
237
|
+
});
|
|
238
|
+
const rest = edits.length > NAMED ? [` and ${edits.length - NAMED} more`] : [];
|
|
239
|
+
return [head, ...lines, ...rest].join("\n");
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* DEPOIS DE SUBIR, O BASELINE PASSA A SER O QUE ESTÁ NO DISCO - a metade que fecha o ciclo.
|
|
243
|
+
*
|
|
244
|
+
* Sem isto, todo `sync` reenviaria a mesma alteração para sempre e o documento na plataforma
|
|
245
|
+
* ganharia uma revisão nova a cada rodada, sem nada ter mudado. É a outra metade de A4, e ela
|
|
246
|
+
* mora aqui - e não no corpo do comando - para poder ser exercida por spec sem subir nada.
|
|
247
|
+
*
|
|
248
|
+
* SÓ DEPOIS DE O SERVIDOR TER ACEITADO: uma recusa mantém o baseline velho, para a próxima
|
|
249
|
+
* rodada tentar de novo em vez de esquecer o que ele fez.
|
|
250
|
+
*/
|
|
251
|
+
/** E a confirmação, depois de o servidor ter aceitado - a única frase no passado. */
|
|
252
|
+
export function landedLine(applied, slug) {
|
|
253
|
+
return `${applied} change${applied === 1 ? "" : "s"} from your files went up to ${slug}`;
|
|
254
|
+
}
|
|
255
|
+
export async function settleBaseline(root, slug, version, document) {
|
|
256
|
+
await writeFile(join(root, "_synthesisui", "ds", slug, `v${version}`, ".installed.json"), `${JSON.stringify(document)}\n`, "utf8");
|
|
257
|
+
}
|