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/CHANGELOG.md +63 -0
- package/README.md +221 -5
- package/dist/index.js +449 -57
- package/package.json +2 -2
- package/rules/00-anti-patterns.md +101 -0
- package/rules/01-modes.md +1 -1
- package/rules/03-patterns.md +34 -0
- package/rules.index.json +105 -0
- package/templates/COMMAND.md.tmpl +242 -14
- package/templates/command-metadata.json +5 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jig-ui",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "A design system for coding agents.
|
|
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
|
|
193
|
+
- **The anti-pattern file.** All 112 rules in it apply everywhere.
|
|
194
194
|
|
|
195
195
|
---
|
|
196
196
|
|
package/rules/03-patterns.md
CHANGED
|
@@ -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
|
]
|