@plus234feed/ds 0.3.0 → 0.4.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.
Files changed (3) hide show
  1. package/README.md +69 -138
  2. package/package.json +3 -2
  3. package/src/components.css +173 -45
package/README.md CHANGED
@@ -1,90 +1,44 @@
1
- # Plus234Feed Design System
1
+ # @plus234feed/ds
2
2
 
3
- Editorial tokens, a named type scale and the components a news intelligence
4
- platform needs. Plain CSS on top of design tokens, no framework, no build step
5
- for consumers.
3
+ The Plus234Feed design system. Editorial tokens, a named type scale and the
4
+ components a news site needs: newspaper hierarchy, accessible colour in both
5
+ themes, plain CSS.
6
6
 
7
- Documentation site: `docs/index.html`, generated from `site/pages/`. Deploy the
8
- `docs/` folder; `vercel.json` already points at it.
7
+ No framework, no JavaScript, no build step for consumers. Four stylesheets and
8
+ four font files.
9
9
 
10
- 27 pages: get started, foundations, one page per component, patterns and the
11
- rules. Each component page says what it is, when to use it, when not to, live
12
- examples with copyable HTML, and the tokens it reads.
13
-
14
- ## Why this exists
15
-
16
- The frontend had 252 component files and 40,638 lines of TSX, but only 25
17
- shared components. Measured on 2026-11-04:
18
-
19
- | Symptom | Count |
20
- | --- | --- |
21
- | `page.tsx` files not using `PageLayout` | 92 (16 do) |
22
- | Files re-implementing an article card | 47 |
23
- | `rounded-*` variants in use | 11 |
24
- | Ad-hoc text sizes in use | 9, dominated by `text-sm` (1,019) and `text-xs` (720) |
25
- | Hardcoded uses of a single green | 27 |
26
- | Variables used but defined nowhere | 5 |
27
-
28
- That last row was a live bug, not a matter of taste. `--text-muted` (42 uses),
29
- `--border` (36), `--bg-elevated` (13), `--bg-secondary` and `--bg-hover` were
30
- referenced with no fallback and declared in no stylesheet, concentrated in
31
- `components/prices/` and the cost of living pages.
32
-
33
- Reader behaviour pointed the same way. Almost every visitor is new, and
34
- almost nobody opens a second page. Readers arriving from search spend real
35
- time on the article, finish it, and leave: the problem is not that they are
36
- bored, it is that the page offers them nothing to do next.
10
+ Documentation: [ds.plus234feed.com](https://ds.plus234feed.com)
37
11
 
38
12
  ## Install
39
13
 
40
14
  ```bash
41
- npm install
42
- npm run build # tokens + gallery
43
- npm test # 13 checks
15
+ npm install @plus234feed/ds
44
16
  ```
45
17
 
18
+ ```css
19
+ @import "@plus234feed/ds/tokens.css"; /* custom properties, both themes */
20
+ @import "@plus234feed/ds/type.css"; /* .p234-headline and friends */
21
+ @import "@plus234feed/ds/components.css"; /* .p234-story and friends */
22
+ @import "@plus234feed/ds/fonts.css"; /* Charter, 64KB, four faces */
46
23
  ```
47
- npm run build:tokens tokens/*.json -> build/*.css, build/tokens.json
48
- npm run build:docs site/pages/** -> docs/**
49
- npm run check build + test, what CI runs
50
- ```
51
-
52
- ### Authoring a docs page
53
24
 
54
- A page is an HTML fragment with a JSON comment at the top, wrapped by
55
- `site/layout.html`. Four authoring elements expand at build time:
25
+ `fonts.css` resolves `./fonts/*.woff2` relative to itself. If your bundler does
26
+ not follow that, copy the faces to a public path and rewrite the four `url()`
27
+ values:
56
28
 
57
- ```html
58
- <!-- { "title": "Story", "section": "components", "order": 10, "description": "..." } -->
59
-
60
- <docs-example label="...">markup</docs-example> preview plus copyable HTML
61
- <docs-tokens names="ink,ink-muted"> generated swatches, both themes
62
- <docs-type roles="display-1,headline"> generated specimens
63
- <docs-scale group="space"> generated scale table
64
- <docs-uses>--p234-color-ink, ...</docs-uses> the tokens a component reads
29
+ ```bash
30
+ cp node_modules/@plus234feed/ds/src/fonts/*.woff2 public/fonts/
31
+ sed 's|url("./fonts/|url("/fonts/|g' \
32
+ node_modules/@plus234feed/ds/src/fonts.css > app/fonts.css
65
33
  ```
66
34
 
67
- Navigation and the section sidebar are derived from the front matter, so a new
68
- page appears in both by existing.
69
-
70
35
  ## Use it
71
36
 
72
- ```css
73
- @import "@plus234feed/ds/fonts.css"; /* Charter, self-hosted, 64KB */
74
- @import "@plus234feed/ds/tokens.css"; /* theme driven by a .dark class */
75
- @import "@plus234feed/ds/type.css"; /* .p234-headline and friends */
76
- @import "@plus234feed/ds/components.css";
77
- ```
78
-
79
- Use `tokens-auto.css` instead of `tokens.css` only on standalone pages that
80
- have no theme toggle. It adds an OS-preference block. The app must not use it:
81
- a reader who picked light has to get light on a dark laptop.
82
-
83
37
  ```html
84
- <a class="p234-story p234-story--lead" href="...">
38
+ <a class="p234-story p234-story--lead" href="/article/...">
85
39
  <span class="p234-kicker" data-section="markets">Market intelligence</span>
86
- <h3 class="p234-story__headline">Pump prices are running 25 per cent above the last official figure</h3>
87
- <p class="p234-story__standfirst">NBS puts Lagos petrol at 1,077 naira in May.</p>
40
+ <h3 class="p234-story__headline">Pump prices are running above the last official figure</h3>
41
+ <p class="p234-story__standfirst">The gap between the two is the story.</p>
88
42
  <div class="p234-meta">
89
43
  <span class="p234-meta__source">Plus234Feed</span>
90
44
  <span class="p234-meta__dot">&middot;</span>
@@ -94,76 +48,53 @@ a reader who picked light has to get light on a dark laptop.
94
48
  </a>
95
49
  ```
96
50
 
97
- ## How it is built
51
+ ## Themes
98
52
 
53
+ `tokens.css` is class-driven: add `dark` or `data-theme="dark"` to the root
54
+ element. It never reads the operating system preference, because a reader who
55
+ chose light should get light on a dark laptop.
56
+
57
+ Use `tokens-auto.css` instead only on a standalone page with no theme control.
58
+
59
+ ## What is in it
60
+
61
+ | | |
62
+ | --- | --- |
63
+ | Tokens | colour, type, space, shape, charts, forms, layering, motion |
64
+ | Type | 17 named roles, serif for reading, tabular sans for figures |
65
+ | Components | story, masthead, footer, prose, chart, map, table, field, and more |
66
+
67
+ Colour is verified rather than eyeballed. Every pair that has to be readable is
68
+ checked in both themes at build time and the build fails on any that is not.
69
+ The chart palette is checked for colour-blind separation using OKLab and the
70
+ Machado simulation, which is also why it stops at the number of series it can
71
+ actually keep distinct instead of inventing more.
72
+
73
+ ## Typeface
74
+
75
+ Charter, self-hosted. Bitstream contributed it to the X Consortium in 1992 with
76
+ permission to use, copy, modify, sublicense, sell and redistribute, provided
77
+ the notice stays intact. That notice ships at `src/fonts/LICENSE.charter.txt`.
78
+ BITSTREAM CHARTER is a registered trademark of Bitstream Inc.
79
+
80
+ ## Developing
81
+
82
+ ```bash
83
+ npm install
84
+ npm run build # tokens and documentation
85
+ npm test # 33 checks
86
+ npm run check # both, which is what CI runs
99
87
  ```
100
- tokens/core.tokens.json light theme, type, space, radius, layout
101
- tokens/themes/dark.tokens.json colour overrides only, enforced
102
- scripts/color.mjs WCAG luminance and contrast maths
103
- scripts/tokens.mjs loading, flattening, alias resolution, the contrast table
104
- scripts/build-tokens.mjs writes build/, fails on any unreadable pair
105
- scripts/build-site.mjs writes the gallery from the built tokens
106
- src/components.css editorial components, every rule reads a token
88
+
89
+ Releases publish from CI through an npm trusted publisher, so there is no token
90
+ in the repository:
91
+
92
+ ```bash
93
+ npm version patch # rebuilds the docs, commits, tags
94
+ git push --follow-tags
107
95
  ```
108
96
 
109
- Token format follows the W3C Design Tokens Community Group draft. Values become
110
- CSS custom properties prefixed `--p234-`.
111
-
112
- ### The contrast gate
113
-
114
- 44 colour pairs are checked in every theme, 88 checks in all, and the build
115
- exits non-zero on any failure. This is the point of the pipeline: a combination
116
- nobody can read cannot reach the site. The first run caught three real
117
- problems, two of which were dark-theme only.
118
-
119
- The focus ring is checked differently. It is two colours, `focus` with
120
- `focus-outer` drawn around it, and neither half clears 3:1 alone. The amber is
121
- 1.64:1 on white and relies on the dark outer ring there; on a dark page the
122
- outer ring recedes and the amber carries it. So the rule is that at least one
123
- half must clear 3:1 on every surface. See `checkFocus`.
124
-
125
- ## Rules the tokens cannot enforce
126
-
127
- 1. **Two ad slots on an article, maximum, and no mobile sticky.** The current
128
- template carries six plus a sticky around a 147-word summary. Mobile is 64
129
- per cent of search traffic.
130
- 2. **Every article ends with a next step that matches why the reader came.** A
131
- visa reader gets visa stories, not the general feed. This is the module the
132
- near-total absence of second pages is asking for.
133
- 3. **No raw hex.** If a colour is missing, add a token.
134
- 4. **No undefined tokens.** A prebuild check fails on any `--p234-` variable
135
- this package does not declare.
136
- 5. **Pick a type role, never a font size.** Seventeen roles cover the site.
137
- Reaching past them means a role is missing, which is a conversation.
138
- 6. **Direction is never colour alone.** Gainers and decliners carry an arrow.
139
- 7. **One lead per page.** If two stories both look like the lead, neither is.
140
-
141
- ## Type
142
-
143
- Roles, not t-shirt sizes. Serif for what a reader reads, sans for what a reader
144
- operates, tabular sans for every figure.
145
-
146
- The serif is **Charter**, self-hosted from `src/fonts/`, 64KB for four faces,
147
- `font-display: swap`. Charter is the face macOS and iOS already carry, which is
148
- why the gallery looked right before a single font was loaded. Shipping it is
149
- what gives Android readers, who are most of the audience, the same page
150
- instead of a Noto Serif fallback.
151
-
152
- Bitstream contributed Charter to the X Consortium in 1992 with permission to
153
- use, copy, modify, sublicense, sell and redistribute, provided the notice stays
154
- intact. That notice is `src/fonts/LICENSE.charter.txt` and ships with the
155
- files. BITSTREAM CHARTER is a registered trademark of Bitstream Inc.
156
-
157
- The sans is still the system stack headed by Inter, which the Next app already
158
- loads. On a machine without Inter it resolves to San Francisco or Roboto. If
159
- that drift matters it should be self-hosted the same way.
160
-
161
- Running text never drops below 16px. `caption` and `kicker` are the only roles
162
- allowed at 12px, and the tests enforce both floors.
163
-
164
- ## Scope
165
-
166
- Phase 1 is this package plus the site chrome and the article template:
167
- almost every session starts on an article, and the masthead and footer appear
168
- on every page, so those are what make the site stop looking like two sites.
169
- Fronts, data pages, account and forms follow.
97
+ ## Licence
98
+
99
+ UNLICENSED. Published so Plus234Feed applications can install it; not offered
100
+ for reuse.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plus234feed/ds",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Plus234Feed design system. Editorial tokens, type scale and components for a news intelligence platform: newspaper hierarchy, accessible colour in both themes, one component per job.",
5
5
  "keywords": [
6
6
  "design-system",
@@ -44,7 +44,8 @@
44
44
  "build:docs": "node scripts/build-docs.mjs",
45
45
  "build": "npm run build:tokens && npm run build:docs",
46
46
  "test": "node --test test/*.test.js",
47
- "check": "npm run build && npm test"
47
+ "check": "npm run build && npm test",
48
+ "version": "npm run build && git add build docs"
48
49
  },
49
50
  "publishConfig": {
50
51
  "access": "public"
@@ -590,76 +590,88 @@
590
590
  }
591
591
 
592
592
  /* --- masthead ----------------------------------------------------------
593
- The 727 line Header replaced by a shell. The dropdown and overlay
594
- behaviour belongs to the app; what lives here is the shape, the rules
595
- and the layering. */
593
+ A newspaper masthead, not an app bar. Three rows:
594
+
595
+ utility search, edition links, account
596
+ brand the wordmark, centred, with date and figures flanking it
597
+ sections the section nav, with a full width panel beneath it
598
+
599
+ The centred wordmark is the whole point: it is what makes the top of the
600
+ page read as a paper rather than a product. */
596
601
  .p234-masthead {
597
602
  position: sticky;
598
603
  top: 0;
599
604
  z-index: var(--p234-z-sticky);
600
605
  background: var(--p234-color-paper);
601
- border-bottom: var(--p234-border-heavy) solid var(--p234-color-ink);
606
+ border-bottom: var(--p234-border-hairline) solid var(--p234-color-rule);
602
607
  }
603
- .p234-masthead__bar {
608
+
609
+ /* Row 1: utility. Hidden on phones, where the bar collapses to one row. */
610
+ .p234-masthead__utility {
611
+ display: none;
612
+ border-bottom: var(--p234-border-hairline) solid var(--p234-color-rule);
613
+ }
614
+ @media (min-width: 62rem) { .p234-masthead__utility { display: block; } }
615
+ .p234-masthead__utility-in {
604
616
  max-width: var(--p234-layout-container);
605
617
  margin: 0 auto;
606
- padding: var(--p234-space-4) var(--p234-layout-gutter);
607
- display: flex;
618
+ padding: var(--p234-space-3) var(--p234-layout-gutter-lg);
619
+ display: grid;
620
+ grid-template-columns: 1fr auto 1fr;
608
621
  align-items: center;
609
622
  gap: var(--p234-space-5);
610
623
  }
611
- @media (min-width: 48rem) {
612
- .p234-masthead__bar { padding-inline: var(--p234-layout-gutter-lg); }
613
- }
614
- .p234-masthead__brand {
624
+ .p234-masthead__editions {
615
625
  display: flex;
616
- align-items: baseline;
617
- gap: var(--p234-space-3);
626
+ justify-content: center;
627
+ gap: var(--p234-space-5);
628
+ }
629
+ .p234-masthead__editions a {
630
+ font-family: var(--p234-type-ui-sm-family);
631
+ font-size: var(--p234-type-ui-sm-size);
632
+ font-weight: 600;
633
+ letter-spacing: 0.04em;
634
+ text-transform: uppercase;
635
+ color: var(--p234-color-ink-muted);
618
636
  text-decoration: none;
619
- color: inherit;
620
- flex-shrink: 0;
637
+ white-space: nowrap;
621
638
  }
622
- /* The wordmark. Two files, not one recoloured image: the E is dark on light
623
- pages and white on dark, so neither a filter nor an opacity trick gets it
624
- right. The pair swaps with the theme. */
639
+ .p234-masthead__editions a:hover,
640
+ .p234-masthead__editions a[aria-current] { color: var(--p234-color-ink); }
641
+
642
+ /* Row 2: the wordmark, centred. */
643
+ .p234-masthead__bar {
644
+ max-width: var(--p234-layout-container);
645
+ margin: 0 auto;
646
+ padding: var(--p234-space-4) var(--p234-layout-gutter);
647
+ display: grid;
648
+ grid-template-columns: 1fr auto 1fr;
649
+ align-items: center;
650
+ gap: var(--p234-space-4);
651
+ }
652
+ @media (min-width: 62rem) {
653
+ .p234-masthead__bar { padding: var(--p234-space-5) var(--p234-layout-gutter-lg); }
654
+ }
655
+ .p234-masthead__brand { justify-self: center; display: block; text-decoration: none; }
625
656
  .p234-masthead__logo { display: block; height: 1.75rem; width: auto; }
657
+ @media (min-width: 62rem) { .p234-masthead__logo { height: 2.5rem; } }
626
658
  .p234-masthead__logo--dark { display: none; }
627
659
  .dark .p234-masthead__logo--light,
628
660
  [data-theme="dark"] .p234-masthead__logo--light { display: none; }
629
661
  .dark .p234-masthead__logo--dark,
630
662
  [data-theme="dark"] .p234-masthead__logo--dark { display: block; }
631
663
 
632
- /* Text fallback, for anywhere the image cannot be used. */
633
- .p234-masthead__mark {
634
- font-family: var(--p234-type-ui-family);
635
- font-weight: 900;
636
- font-size: 1.25rem;
637
- letter-spacing: -0.03em;
638
- color: var(--p234-color-brand);
639
- }
640
- .p234-masthead__tagline {
641
- font-family: var(--p234-type-caption-family);
642
- font-size: var(--p234-type-caption-size);
643
- color: var(--p234-color-ink-muted);
664
+ .p234-masthead__left { display: flex; align-items: center; gap: var(--p234-space-3); }
665
+ .p234-masthead__right { display: flex; align-items: center; justify-content: flex-end; gap: var(--p234-space-3); }
666
+ .p234-masthead__dateline {
644
667
  display: none;
645
- }
646
- @media (min-width: 62rem) { .p234-masthead__tagline { display: inline; } }
647
-
648
- .p234-masthead__nav { display: none; gap: var(--p234-space-5); margin-left: auto; }
649
- @media (min-width: 62rem) { .p234-masthead__nav { display: flex; } }
650
- .p234-masthead__nav a {
651
- font-family: var(--p234-type-ui-family);
652
- font-size: var(--p234-type-ui-size);
653
- font-weight: 600;
668
+ font-family: var(--p234-type-meta-family);
669
+ font-size: var(--p234-type-meta-size);
654
670
  color: var(--p234-color-ink-muted);
655
- text-decoration: none;
656
- white-space: nowrap;
671
+ line-height: 1.3;
657
672
  }
658
- .p234-masthead__nav a:hover { color: var(--p234-color-ink); }
659
- .p234-masthead__nav a[aria-current] { color: var(--p234-color-ink); }
673
+ @media (min-width: 62rem) { .p234-masthead__dateline { display: block; } }
660
674
 
661
- .p234-masthead__actions { display: flex; align-items: center; gap: var(--p234-space-3); margin-left: auto; }
662
- @media (min-width: 62rem) { .p234-masthead__actions { margin-left: 0; } }
663
675
  .p234-masthead__icon {
664
676
  display: inline-flex;
665
677
  align-items: center;
@@ -674,6 +686,122 @@
674
686
  }
675
687
  .p234-masthead__icon:hover { background: var(--p234-color-paper-sunk); }
676
688
 
689
+ /* Phone only: the hamburger and the compact sign in. On desktop the utility
690
+ row carries these, so showing them twice is just clutter. */
691
+ @media (min-width: 62rem) {
692
+ .p234-masthead__menubtn,
693
+ .p234-masthead__compact { display: none !important; }
694
+ }
695
+
696
+ /* Row 3: the sections. Centred on desktop, scrolls sideways on a phone. */
697
+ .p234-masthead__sections {
698
+ border-top: var(--p234-border-hairline) solid var(--p234-color-rule);
699
+ overflow-x: auto;
700
+ scrollbar-width: none;
701
+ }
702
+ .p234-masthead__sections::-webkit-scrollbar { display: none; }
703
+ .p234-masthead__sections-in {
704
+ max-width: var(--p234-layout-container);
705
+ margin: 0 auto;
706
+ padding: 0 var(--p234-layout-gutter);
707
+ display: flex;
708
+ gap: var(--p234-space-6);
709
+ }
710
+ @media (min-width: 62rem) {
711
+ .p234-masthead__sections-in { justify-content: center; padding-inline: var(--p234-layout-gutter-lg); }
712
+ }
713
+ .p234-masthead__section {
714
+ display: inline-flex;
715
+ align-items: center;
716
+ gap: var(--p234-space-2);
717
+ flex-shrink: 0;
718
+ background: none;
719
+ border: 0;
720
+ cursor: pointer;
721
+ padding: var(--p234-space-4) 0;
722
+ font-family: var(--p234-type-ui-family);
723
+ font-size: var(--p234-type-ui-size);
724
+ font-weight: 500;
725
+ color: var(--p234-color-ink);
726
+ text-decoration: none;
727
+ white-space: nowrap;
728
+ border-bottom: var(--p234-border-thick) solid transparent;
729
+ margin-bottom: -1px;
730
+ }
731
+ .p234-masthead__section:hover { border-bottom-color: var(--p234-color-rule-strong); }
732
+ .p234-masthead__section[aria-expanded="true"],
733
+ .p234-masthead__section[aria-current] { border-bottom-color: var(--p234-color-ink); }
734
+
735
+ /* The panel. Full width of the viewport, not a box anchored to a button:
736
+ a dropdown pinned under the last nav item runs off the right edge. */
737
+ .p234-megamenu {
738
+ position: absolute;
739
+ left: 0;
740
+ right: 0;
741
+ top: 100%;
742
+ z-index: var(--p234-z-dropdown);
743
+ background: var(--p234-color-paper);
744
+ border-top: var(--p234-border-hairline) solid var(--p234-color-rule);
745
+ border-bottom: var(--p234-border-hairline) solid var(--p234-color-rule);
746
+ box-shadow: var(--p234-shadow-overlay);
747
+ }
748
+ .p234-megamenu__in {
749
+ max-width: var(--p234-layout-container);
750
+ margin: 0 auto;
751
+ padding: var(--p234-space-7) var(--p234-layout-gutter-lg);
752
+ display: grid;
753
+ gap: var(--p234-space-7);
754
+ grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
755
+ }
756
+ .p234-megamenu ul { list-style: none; margin: 0; padding: 0; }
757
+ .p234-megamenu li { margin-bottom: var(--p234-space-4); }
758
+ .p234-megamenu a {
759
+ font-family: var(--p234-type-ui-family);
760
+ font-size: var(--p234-type-ui-size);
761
+ color: var(--p234-color-ink);
762
+ text-decoration: none;
763
+ }
764
+ .p234-megamenu a:hover { text-decoration: underline; text-underline-offset: 2px; }
765
+
766
+ /* The phone drawer: a full screen panel, search first, then the sections as
767
+ accordions. <details> so it works with no JavaScript. */
768
+ .p234-drawer {
769
+ position: fixed;
770
+ inset: 0;
771
+ z-index: var(--p234-z-overlay);
772
+ background: var(--p234-color-paper);
773
+ overflow-y: auto;
774
+ padding: var(--p234-space-5) var(--p234-layout-gutter) var(--p234-space-9);
775
+ }
776
+ .p234-drawer__head { display: flex; justify-content: flex-start; margin-bottom: var(--p234-space-6); }
777
+ .p234-drawer__search { display: flex; gap: var(--p234-space-3); margin-bottom: var(--p234-space-6); }
778
+ .p234-drawer__group { border-top: var(--p234-border-hairline) solid var(--p234-color-rule); }
779
+ .p234-drawer__group > summary {
780
+ list-style: none;
781
+ display: flex;
782
+ align-items: center;
783
+ justify-content: space-between;
784
+ padding: var(--p234-space-5) 0;
785
+ cursor: pointer;
786
+ font-family: var(--p234-type-headline-xs-family);
787
+ font-size: var(--p234-type-headline-xs-size);
788
+ font-weight: var(--p234-type-headline-xs-weight);
789
+ color: var(--p234-color-ink);
790
+ }
791
+ .p234-drawer__group > summary::-webkit-details-marker { display: none; }
792
+ .p234-drawer__links {
793
+ display: grid;
794
+ grid-template-columns: 1fr 1fr;
795
+ gap: var(--p234-space-4);
796
+ padding-bottom: var(--p234-space-5);
797
+ }
798
+ .p234-drawer__links a {
799
+ font-family: var(--p234-type-ui-family);
800
+ font-size: var(--p234-type-ui-size);
801
+ color: var(--p234-color-ink);
802
+ text-decoration: none;
803
+ }
804
+
677
805
  /* --- subnav ------------------------------------------------------------
678
806
  Section level navigation: the price pages, the account panes. Scrolls
679
807
  sideways on a phone rather than wrapping into two rows. */