jig-ui 0.13.0 → 0.14.0

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "jig-ui",
3
- "version": "0.13.0",
4
- "description": "A design system for coding agents. 115 numbered UI rules, brand x mode design tokens, and an installer for Claude Code, Codex, Cursor and opencode.",
3
+ "version": "0.14.0",
4
+ "description": "A design system for coding agents. 130 numbered UI rules, brand x mode design tokens, and an installer for Claude Code, Codex, Cursor and opencode.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
7
7
  "bin": {
@@ -574,6 +574,107 @@ Load `05-copy.md` whenever writing or reviewing a user-facing string.
574
574
 
575
575
  ---
576
576
 
577
+ ## J. Search and sharing
578
+
579
+ What a stranger meets **before** the page: the search result, the link preview in
580
+ a message, the card a colleague pastes into a channel. Nobody reads `<head>`, so
581
+ it is the surface that drifts, and it is the one that is read first.
582
+
583
+ **Which pages this section applies to is decided by mode, not by taste.**
584
+ `editorial` is first-visit content and is indexable. `product` and `operator` are
585
+ what somebody reaches after signing in, and are not: an admin screen in a search
586
+ result is an invitation, and a login page in one invites credential stuffing. A
587
+ spec overrides the default per page — a CV shared by link, a thank-you page, a
588
+ not-found page — and says why.
589
+
590
+ ### J-120 Metadata that no longer matches the page
591
+ ❌ A title or description still carrying positioning the page dropped, changed in a later commit "when there is time"
592
+ ✅ Change the metadata **in the same change as the copy**. A page's title, description and preview text are copy, written by whoever wrote the headline, reviewed the same way.
593
+ This is the whole reason the section exists. A copy pass reads pages; nobody reads `<head>`. One site rewrote every visible page and served the old positioning to search for a day — including a metaphor whose explanation had been deleted the day before, so the stranger who met it had nowhere left to resolve it. The comment above that line already said "metadata is copy, when the positioning moves this moves with it". It had caught the problem once before and did not catch it again, because a comment is not a check.
594
+
595
+ ### J-121 An indexable page with no title or description of its own
596
+ ❌ A route inheriting the site-wide title, or carrying none, so the result page shows a truncated URL or an excerpt of the navigation
597
+ ✅ Every indexable page has its own `<title>` and meta description. The home page owns the site default; nothing else inherits it silently.
598
+ A missing description does not leave the slot empty. The search engine writes one, from whatever text it finds first, which is usually the navigation.
599
+
600
+ ### J-122 Metadata past its budget
601
+ ❌ A 78-character title, a 210-character description, both eyeballed
602
+ ✅ Title ≤ 60 characters, description ≤ 155, measured. Past the budget the end is cut, and the cut lands mid-sentence.
603
+ The budget is not a style preference; it is the width of the box someone else renders. Write the important half first, so a cut costs the least.
604
+
605
+ ### J-123 A page that must not be indexed and does not say so
606
+ ❌ An admin screen, a sign-in page, an internal tool with no robots directive, kept out of search by nothing but obscurity
607
+ ✅ `noindex` on the page itself, from its own metadata. In `product` and `operator` this is the default and its absence is the defect.
608
+ `robots.txt` is not this. It is public, advisory, and read by strangers as a list of interesting places: naming `/admin` there tells everyone where it is. A path is safe to name only when something else protects it — a session guard, an authenticating API — and never because the file asked politely.
609
+
610
+ ### J-124 A sitemap that contradicts the page
611
+ ❌ A route listed in the sitemap whose own metadata says `noindex`; a sitemap entry for a page that does not exist
612
+ ✅ One answer per route. The sitemap lists what is indexable, and nothing else.
613
+ Contradicting yourself in two files tells a crawler you do not know which is true, and it will decide for you.
614
+
615
+ ### J-125 Invented facts in metadata
616
+ ❌ `lastModified: new Date()` in a sitemap; a `datePublished` filled in because the field existed; an author, rating or price nobody supplied
617
+ ✅ Emit a field only where a real value exists in the content. Leave it out otherwise.
618
+ A date that is today's on every request is false on every request, and repeated daily it teaches the crawler to disbelieve the field. Structured data is a claim about facts, and a wrong one is worse than a missing one.
619
+
620
+ ### J-126 Structured data retyped instead of read
621
+ ❌ A name, email or description written as a literal in JSON-LD beside the same value in the content model
622
+ ✅ Build it from the same source the page renders from. One value, one place.
623
+ A second copy of the positioning is a second thing to keep in step by hand, which is `J-120` again wearing a different hat.
624
+
625
+ ### J-127 A list served as a fact when it is empty
626
+ ❌ A sitemap that renders with no entries because a content read failed, asserting the site has nothing
627
+ ✅ Concatenate the static routes unconditionally, and let a failed read yield the stale list rather than an empty one. When a list is genuinely empty, prove it before shipping.
628
+ An empty sitemap is not a missing sitemap. It is a positive claim, and the crawler believes it.
629
+
630
+ ---
631
+
632
+ ## K. Safety at the interface
633
+
634
+ **This is not a security review, and nothing here should be read as one.** Jig
635
+ sees interfaces. It knows nothing about your sessions, your rate limits, your
636
+ CORS origins, your secrets or your dependencies, and a clean run says nothing
637
+ about any of them. What it can see is the handful of things an interface does to
638
+ itself — the ones that ship because nobody looks at the markup with this question
639
+ in mind.
640
+
641
+ ### K-128 A new-tab link that hands over the page it left
642
+ ❌ `target="_blank"` with no `rel`
643
+ ✅ `rel="noopener"` on every `target="_blank"`, `noreferrer` too when the destination has no business knowing where the reader came from.
644
+ The opened page gets a handle on the window that opened it and can navigate it somewhere else. The reader comes back to a tab that looks like yours and is not. Modern browsers imply `noopener` for `_blank`, which is the argument for writing it rather than against: the ones that do not are the ones being attacked.
645
+
646
+ ### K-129 User content written as markup
647
+ ❌ `dangerouslySetInnerHTML`, `v-html`, `innerHTML =`, `{@html}` carrying anything a person typed
648
+ ✅ Render it as text. Where formatting is genuinely required, sanitise on the way in with a library that is maintained, and keep the allowed set to what the feature needs.
649
+ The name of the React prop is a warning someone wrote on purpose. A comment, a display name, a product description: each is a place a script arrives and runs with your origin's privileges.
650
+
651
+ ### K-130 Credential fields that fight the password manager
652
+ ❌ `autocomplete="off"` on a password, a `paste` handler that blocks pasting, a one-time-code field with no `autocomplete`
653
+ ✅ `autocomplete="current-password"`, `"new-password"`, `"one-time-code"`, and nothing preventing paste.
654
+ Blocking the manager does not stop an attacker; it stops the reader using a long unique password, so they type a short one they can remember and reuse it everywhere. The interface decides which of those two happens.
655
+
656
+ ### K-131 A frame with no sandbox, a script from anywhere
657
+ ❌ `<iframe src="https://third-party">` with no `sandbox`; a `<script src>` pointing at an origin nobody chose, on a page that takes payments or credentials
658
+ ✅ `sandbox` with only the capabilities the embed needs, and `allow` narrowed the same way. Third-party script on a sensitive page is a decision, recorded with a reason (`DECISIONS.md`), not a default.
659
+ An embedded frame runs somebody else's code inside your page, and a script tag hands them the same origin your session lives in. Both are sometimes right; neither is ever automatic.
660
+
661
+ ### K-132 A secret rendered as plain text
662
+ ❌ An API key, a recovery code or a token printed into the page, sitting in the DOM for anything that reads it
663
+ ✅ Show it once, deliberately, behind an action the reader takes, with a copy control and a clear statement that it will not be shown again.
664
+ Anything on the screen is in the DOM, in the accessibility tree, in a screenshot, and often in a session recording nobody remembered was running.
665
+
666
+ ### K-133 An error that describes the system
667
+ ❌ A stack trace, a database error, a file path or a framework name shown to whoever hit the page
668
+ ✅ Say what happened in the reader's terms and what to do next (`05-copy.md`). Keep the detail in the log, where it is useful and not public.
669
+ An error is copy, and the audience is the person reading it. Naming the stack tells a stranger which list of known problems to work through.
670
+
671
+ ### K-134 Inline handlers on a page with a content policy
672
+ ❌ `onclick="…"` in markup, a `<script>` with no nonce, on a site that sets a Content-Security-Policy
673
+ ✅ Bind behaviour in script (`E-33` asks for a real control anyway), and let the policy's nonce cover the one bootstrap the framework emits.
674
+ This is where a security decision made in configuration lands on whoever writes the markup: under a strict policy the inline handler simply does not run, and the page fails in the browser rather than in a check.
675
+
676
+ ---
677
+
577
678
  ## L-04 · Self-check before finishing
578
679
 
579
680
  Run this against what you produced. Any "no" is a defect to fix, not a note to mention.
package/rules/01-modes.md CHANGED
@@ -190,7 +190,7 @@ Attempting to vary these by mode is a category error:
190
190
  - **Accessibility floors.** Contrast, focus indication, target size, semantic markup. Identical in all three. `operator` being dense does not license a 24px tap target or a 3:1 body contrast.
191
191
  - **Brand identity.** Palette, typeface, logo, voice.
192
192
  - **State completeness.** Every mode renders loading, empty, error and disabled.
193
- - **The anti-pattern file.** All 97 rules in it apply everywhere.
193
+ - **The anti-pattern file.** All 112 rules in it apply everywhere.
194
194
 
195
195
  ---
196
196
 
@@ -524,6 +524,40 @@ Blur the design, zoom out, or step back. You should still be able to tell what t
524
524
 
525
525
  **When nothing can render it, use the analogue:** if all type were one size and one colour, would the layout still communicate its order? If the hierarchy depends entirely on type styling, it is too weak. The analogue is a fallback, not an equal — a reading of the source is not a look at the page.
526
526
 
527
+ ### Step 6 — Decide how it collapses
528
+
529
+ A spec writes a composition per size. This step is what happens **between** them:
530
+ the same content, arranged for less room. Six rules, and the first is the one
531
+ everything else follows from.
532
+
533
+ 1. **Reduce the count, never the size.** Four columns become two, then one; each
534
+ column keeps its own minimum width. Three columns at a third of the width are
535
+ still three columns, and that is the failure `D-111` names — a shrunken
536
+ desktop rather than a composition. The same applies to a row of controls: it
537
+ wraps, stacks or moves behind one control, and it does not get smaller.
538
+ 2. **Order survives.** What is read first at the widest size is read first at the
539
+ narrowest. Collapsing rearranges space, not meaning, and the markup order is
540
+ the reading order at every width (`H-119`).
541
+ 3. **Distinction survives.** If one item was emphasised — a recommended plan, a
542
+ current step — it is still distinguishable after the collapse. Emphasis
543
+ carried by position alone disappears the moment everything is in one column,
544
+ so it needs structure too (`E-91`).
545
+ 4. **Type comes from the scale, not from breakpoints.** The heading tokens are
546
+ fluid: they track the viewport between a floor and a ceiling with no media
547
+ query at all. A hand-written ladder of sizes per breakpoint reintroduces the
548
+ jumps the scale exists to remove, and goes stale the moment the scale changes.
549
+ 5. **Content that is genuinely wider scrolls inside itself.** A code block, a
550
+ wide table, a long identifier: its own container scrolls with a visible edge
551
+ (`E-62`), keeps its type size, and the page never scrolls sideways (`D-115`).
552
+ `editorial` allows no scrolling region on mobile at all (`M-01`).
553
+ 6. **Each transition happens where the content needs it.** The width at which
554
+ three columns stop fitting is a property of the columns, not of a device. Set
555
+ it from the content, and expect the numbers to differ per component.
556
+
557
+ A collapse that satisfies all six looks like the same page with less room. One
558
+ that fails them looks like a different, worse page that happens to share its
559
+ content.
560
+
527
561
  ---
528
562
 
529
563
  ## L-02 · Building modularly
package/rules.index.json CHANGED
@@ -807,5 +807,110 @@
807
807
  "severity": "warning",
808
808
  "since": "0.13.0",
809
809
  "detector": "semantic-element"
810
+ },
811
+ {
812
+ "id": "J-120",
813
+ "bucket": "judgment",
814
+ "severity": "error",
815
+ "since": "0.14.0",
816
+ "pass": "code"
817
+ },
818
+ {
819
+ "id": "J-121",
820
+ "bucket": "mechanical",
821
+ "severity": "error",
822
+ "since": "0.14.0",
823
+ "detector": "metadata"
824
+ },
825
+ {
826
+ "id": "J-122",
827
+ "bucket": "mechanical",
828
+ "severity": "warning",
829
+ "since": "0.14.0",
830
+ "detector": "metadata"
831
+ },
832
+ {
833
+ "id": "J-123",
834
+ "bucket": "mechanical",
835
+ "severity": "error",
836
+ "since": "0.14.0",
837
+ "detector": "metadata"
838
+ },
839
+ {
840
+ "id": "J-124",
841
+ "bucket": "judgment",
842
+ "severity": "error",
843
+ "since": "0.14.0",
844
+ "pass": "code"
845
+ },
846
+ {
847
+ "id": "J-125",
848
+ "bucket": "mechanical",
849
+ "severity": "warning",
850
+ "since": "0.14.0",
851
+ "detector": "metadata"
852
+ },
853
+ {
854
+ "id": "J-126",
855
+ "bucket": "judgment",
856
+ "severity": "note",
857
+ "since": "0.14.0",
858
+ "pass": "code"
859
+ },
860
+ {
861
+ "id": "J-127",
862
+ "bucket": "judgment",
863
+ "severity": "note",
864
+ "since": "0.14.0",
865
+ "pass": "code"
866
+ },
867
+ {
868
+ "id": "K-128",
869
+ "bucket": "mechanical",
870
+ "severity": "error",
871
+ "since": "0.14.0",
872
+ "detector": "interface-safety"
873
+ },
874
+ {
875
+ "id": "K-129",
876
+ "bucket": "mechanical",
877
+ "severity": "warning",
878
+ "since": "0.14.0",
879
+ "detector": "interface-safety"
880
+ },
881
+ {
882
+ "id": "K-130",
883
+ "bucket": "mechanical",
884
+ "severity": "warning",
885
+ "since": "0.14.0",
886
+ "detector": "interface-safety"
887
+ },
888
+ {
889
+ "id": "K-131",
890
+ "bucket": "mechanical",
891
+ "severity": "warning",
892
+ "since": "0.14.0",
893
+ "detector": "interface-safety"
894
+ },
895
+ {
896
+ "id": "K-132",
897
+ "bucket": "judgment",
898
+ "severity": "note",
899
+ "since": "0.14.0",
900
+ "pass": "code"
901
+ },
902
+ {
903
+ "id": "K-133",
904
+ "bucket": "judgment",
905
+ "severity": "warning",
906
+ "since": "0.14.0",
907
+ "pass": "code"
908
+ },
909
+ {
910
+ "id": "K-134",
911
+ "bucket": "judgment",
912
+ "severity": "note",
913
+ "since": "0.14.0",
914
+ "pass": "code"
810
915
  }
811
916
  ]