@codecavepro/brand 1.4.0 → 1.6.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/README.md CHANGED
@@ -45,7 +45,7 @@ redesign turns into a find-and-replace across your codebase.
45
45
 
46
46
  ### Already have your own base styles? Import the values only
47
47
 
48
- `@codecavepro/brand/css` is the design system **whole**: the tokens, six `@font-face`
48
+ `@codecavepro/brand/css` is the design system **whole**: the tokens, ten `@font-face`
49
49
  rules, base rules for `html`, `body`, `h1`–`h6` and `a`, two layout primitives and
50
50
  around sixty component classes. That is what you want for a page that should look like
51
51
  CODECAVE. It is *not* what you want in an app that already has a base layer — dropping
@@ -78,7 +78,7 @@ file at that URL are provably the same bytes.
78
78
 
79
79
  ## Fonts: this package ships none
80
80
 
81
- `@codecavepro/brand/css` declares six `@font-face` rules for **Satoshi**, but **no font
81
+ `@codecavepro/brand/css` declares ten `@font-face` rules for **Satoshi**, but **no font
82
82
  binaries are included** — that is a licensing question, not an oversight. Until you
83
83
  supply the files, the faces 404 and the browser falls back down the stack
84
84
  (`-apple-system`, `Segoe UI`, Roboto, …). Tokens, colours and the type *scale* are all
@@ -93,10 +93,17 @@ dropped into a project on its own:
93
93
  | `@codecavepro/brand/fonts.css` | `./Satoshi-*.woff2` | **beside** the stylesheet itself |
94
94
  | `@codecavepro/brand/tokens.css` | *none — it declares no faces* | wherever your own `@font-face` rules point |
95
95
 
96
- Get the cuts from [Fontshare](https://www.fontshare.com/fonts/satoshi) — Light 300,
97
- Regular 400, Italic, Medium 500, Bold 700, Black 900. Bind each with a real
98
- `font-weight` descriptor rather than letting the browser synthesize; the design system
99
- documents why in [DESIGN.md §10.3](https://github.com/CodeCavePro/brand/blob/development/docs/DESIGN.md#103-synthesized-vs-real-font-weights).
96
+ Get the cuts from [Fontshare](https://www.fontshare.com/fonts/satoshi) — 300, 400, 500,
97
+ 700 and 900, each upright and italic, which is the ten faces these stylesheets declare.
98
+ Bind each with a real `font-weight` descriptor rather than letting the browser
99
+ synthesize; the design system documents why in
100
+ [DESIGN.md §10.3](https://github.com/CodeCavePro/brand/blob/development/docs/DESIGN.md#103-synthesized-vs-real-font-weights).
101
+
102
+ **Do not use the `stylesheet.css` that comes in the Fontshare download.** It declares
103
+ Bold and Black *both* as `font-weight: bold`, and their italics likewise, so four cuts
104
+ collide into two slots and whichever is declared last silently wins — your `900` text
105
+ renders Bold, or your `700` renders Black, with nothing to indicate which. Take the
106
+ binaries from that download and the declarations from here.
100
107
 
101
108
  ## The typed module
102
109
 
@@ -186,15 +193,45 @@ you need both:
186
193
  @import "@codecavepro/brand/theme.css";
187
194
  ```
188
195
 
189
- **That order is load-bearing, and `tokens.css` must stay unlayered.** Several entries in
190
- `theme.css` are deliberate self-references (`--color-glow-25: var(--color-glow-25)`):
191
- the declaration is what makes Tailwind emit the utility, while the value comes from
192
- `tokens.css`, whose unlayered `:root` outranks `@layer theme`. Wrap `tokens.css` in a
193
- cascade layer — or leave it out — and those names resolve to nothing.
196
+ **`tokens.css` must stay unlayered.** Several entries in `theme.css` are deliberate
197
+ self-references (`--color-glow-25: var(--color-glow-25)`): the declaration is what makes
198
+ Tailwind emit the utility, while the value comes from `tokens.css`, whose unlayered
199
+ `:root` outranks `@layer theme`. Wrap `tokens.css` in a cascade layer — or leave it out —
200
+ and those names resolve to nothing. Measured on codecave.pro: importing it as
201
+ `layer(brand)` changes the emitted CSS and moves a stylesheet's content hash.
202
+
203
+ The order of those two lines, by contrast, is convention rather than a constraint —
204
+ Tailwind collects every `@theme` and `:root` in the stylesheet before it emits anything,
205
+ so reversing them produced byte-identical output. They are written values-then-names
206
+ because that is the direction of the dependency.
194
207
 
195
208
  Import only `tokens.css` and the components mount and behave correctly and render
196
209
  nearly unstyled.
197
210
 
211
+ ### Installing the components? Tailwind cannot see them
212
+
213
+ **Tailwind's automatic content detection skips `node_modules`, and this package ships Vue
214
+ source.** So a utility class used only inside a component you install exists nowhere
215
+ Tailwind reads, and is simply never emitted. Add the package to the scan:
216
+
217
+ ```css
218
+ @source "../../node_modules/@codecavepro/brand/dist/src";
219
+ ```
220
+
221
+ Adjust the path to be relative to the stylesheet the `@source` sits in. It **adds** to the
222
+ automatic scan rather than narrowing it.
223
+
224
+ This is the failure mode to know about because nothing reports it: the build succeeds, a
225
+ typecheck reports no errors, and the affected elements render unstyled. Leaving the line
226
+ out while installing `Button.vue` alone cost codecave.pro 783 bytes of CSS and all 12 of
227
+ the utilities only that component uses — every button on the site, silently.
228
+
229
+ Because nothing reports it, it is worth checking rather than remembering. codecave.pro
230
+ does it by asking Tailwind twice: its own scanner for the class names inside this
231
+ package, its own compiler for which of those are real utilities, and then whether each
232
+ one is in the CSS the build emitted. That reads the same answer Tailwind would, so it
233
+ cannot go stale when this package gains a component.
234
+
198
235
  ### The content-shaped four resolve their own image URLs
199
236
 
200
237
  `ArticlePreview`, `Review`, `pain-points-item` and `technologies` render CMS content, so
@@ -19,12 +19,11 @@
19
19
  /* ---------------------------------------------------------------------------
20
20
  * Font faces
21
21
  *
22
- * PROVENANCE NOTE this differs from production on purpose.
23
- * codecave.pro ships exactly ONE real cut (Satoshi-Regular.ttf, weight 400)
24
- * and lets the browser synthesize 300 and 700. This package ships the real
25
- * Satoshi cuts captured in the brand asset upload, so bound weights render
26
- * genuinely rather than faux-bolded. Designs built here will look very
27
- * slightly tighter at 700 than the live site does today.
22
+ * The ten faces are bound to explicit numeric weights in both styles. The
23
+ * vendor's own stylesheet.css declares Bold and Black as the same
24
+ * `font-weight: bold`, and their italics likewise, so four cuts collide into
25
+ * two slots there and the last one declared silently wins. That is the defect
26
+ * these declarations exist to avoid; do not replace them with the vendor file.
28
27
  * ------------------------------------------------------------------------ */
29
28
 
30
29
  @font-face {
@@ -36,6 +35,15 @@
36
35
  font-display: swap;
37
36
  }
38
37
 
38
+ @font-face {
39
+ font-family: Satoshi;
40
+ src: url("./fonts/Satoshi-LightItalic.woff2") format("woff2"),
41
+ url("./fonts/Satoshi-LightItalic.woff") format("woff");
42
+ font-weight: 300;
43
+ font-style: italic;
44
+ font-display: swap;
45
+ }
46
+
39
47
  @font-face {
40
48
  font-family: Satoshi;
41
49
  src: url("./fonts/Satoshi-Regular.woff2") format("woff2"),
@@ -63,6 +71,15 @@
63
71
  font-display: swap;
64
72
  }
65
73
 
74
+ @font-face {
75
+ font-family: Satoshi;
76
+ src: url("./fonts/Satoshi-MediumItalic.woff2") format("woff2"),
77
+ url("./fonts/Satoshi-MediumItalic.woff") format("woff");
78
+ font-weight: 500;
79
+ font-style: italic;
80
+ font-display: swap;
81
+ }
82
+
66
83
  @font-face {
67
84
  font-family: Satoshi;
68
85
  src: url("./fonts/Satoshi-Bold.woff2") format("woff2"),
@@ -72,6 +89,15 @@
72
89
  font-display: swap;
73
90
  }
74
91
 
92
+ @font-face {
93
+ font-family: Satoshi;
94
+ src: url("./fonts/Satoshi-BoldItalic.woff2") format("woff2"),
95
+ url("./fonts/Satoshi-BoldItalic.woff") format("woff");
96
+ font-weight: 700;
97
+ font-style: italic;
98
+ font-display: swap;
99
+ }
100
+
75
101
  @font-face {
76
102
  font-family: Satoshi;
77
103
  src: url("./fonts/Satoshi-Black.woff2") format("woff2"),
@@ -81,6 +107,15 @@
81
107
  font-display: swap;
82
108
  }
83
109
 
110
+ @font-face {
111
+ font-family: Satoshi;
112
+ src: url("./fonts/Satoshi-BlackItalic.woff2") format("woff2"),
113
+ url("./fonts/Satoshi-BlackItalic.woff") format("woff");
114
+ font-weight: 900;
115
+ font-style: italic;
116
+ font-display: swap;
117
+ }
118
+
84
119
  :root {
85
120
  /* =========================================================================
86
121
  * RAW RAMPS — verbatim from global.css :root after the 2026 palette
package/dist/fonts.css CHANGED
@@ -4,16 +4,17 @@
4
4
  * package root. This file exists so `fonts/` can be dropped into another
5
5
  * project on its own; the paths here are relative to fonts/ itself.
6
6
  *
7
- * PROVENANCE
8
- * The production site (codecave.pro/src/styles/global.css) declares ONE face:
7
+ * WHY THE WEIGHTS ARE NUMBERS
8
+ * The vendor's own stylesheet.css cannot be used: it declares Bold and Black
9
+ * BOTH as `font-weight: bold`, and their italics likewise, so four cuts
10
+ * collide into two slots and whichever is declared last silently wins. The
11
+ * ten faces below bind 300/400/500/700/900 in each style explicitly, which is
12
+ * the whole point of shipping real cuts rather than letting a browser
13
+ * synthesize them.
9
14
  *
10
- * @font-face { font-family: Satoshi; src: url("../assets/fonts/Satoshi-Regular.ttf"); }
11
- *
12
- * with no font-weight descriptor, which is why live 300 and 700 text is
13
- * browser-synthesized rather than a real cut. The brand asset upload supplied
14
- * genuine Light / Regular / Medium / Bold / Black cuts, so this package binds
15
- * them properly. Expect package output at 700 to be marginally tighter than
16
- * the live site until production ships the same files.
15
+ * The binaries are not in the npm package -- see the note in
16
+ * packages/brand/scripts/build.mjs -- so a consumer supplies the files and
17
+ * these ten declarations tell it what to supply.
17
18
  */
18
19
 
19
20
  @font-face {
@@ -25,6 +26,15 @@
25
26
  font-display: swap;
26
27
  }
27
28
 
29
+ @font-face {
30
+ font-family: Satoshi;
31
+ src: url("./Satoshi-LightItalic.woff2") format("woff2"),
32
+ url("./Satoshi-LightItalic.woff") format("woff");
33
+ font-weight: 300;
34
+ font-style: italic;
35
+ font-display: swap;
36
+ }
37
+
28
38
  @font-face {
29
39
  font-family: Satoshi;
30
40
  src: url("./Satoshi-Regular.woff2") format("woff2"),
@@ -52,6 +62,15 @@
52
62
  font-display: swap;
53
63
  }
54
64
 
65
+ @font-face {
66
+ font-family: Satoshi;
67
+ src: url("./Satoshi-MediumItalic.woff2") format("woff2"),
68
+ url("./Satoshi-MediumItalic.woff") format("woff");
69
+ font-weight: 500;
70
+ font-style: italic;
71
+ font-display: swap;
72
+ }
73
+
55
74
  @font-face {
56
75
  font-family: Satoshi;
57
76
  src: url("./Satoshi-Bold.woff2") format("woff2"),
@@ -61,6 +80,15 @@
61
80
  font-display: swap;
62
81
  }
63
82
 
83
+ @font-face {
84
+ font-family: Satoshi;
85
+ src: url("./Satoshi-BoldItalic.woff2") format("woff2"),
86
+ url("./Satoshi-BoldItalic.woff") format("woff");
87
+ font-weight: 700;
88
+ font-style: italic;
89
+ font-display: swap;
90
+ }
91
+
64
92
  @font-face {
65
93
  font-family: Satoshi;
66
94
  src: url("./Satoshi-Black.woff2") format("woff2"),
@@ -69,3 +97,12 @@
69
97
  font-style: normal;
70
98
  font-display: swap;
71
99
  }
100
+
101
+ @font-face {
102
+ font-family: Satoshi;
103
+ src: url("./Satoshi-BlackItalic.woff2") format("woff2"),
104
+ url("./Satoshi-BlackItalic.woff") format("woff");
105
+ font-weight: 900;
106
+ font-style: italic;
107
+ font-display: swap;
108
+ }
@@ -28,7 +28,7 @@ const imageUrl = (url: string) => props.resolveImage?.(url) ?? url
28
28
  </script>
29
29
 
30
30
  <template>
31
- <a :href="`${paths.insights}/${article.slug}`" :class="`mx-1 lg:mx-2 w-full h-full self-start sm:self-auto p-6 flex flex-col gap-5 sm:gap-8
31
+ <a :href="`${paths.insights}${article.slug}/`" :class="`mx-1 lg:mx-2 w-full h-full self-start sm:self-auto p-6 flex flex-col gap-5 sm:gap-8
32
32
  rounded-[2.25rem] bg-surface-secondary hover:bg-surface-secondary transition-colors cursor-pointer border-surface-tertiary border
33
33
  ${className ?? ''}`">
34
34
  <div class="flex flex-col sm:flex-row gap-5 sm:gap-8 h-fit">
@@ -47,6 +47,6 @@ export const reviews: Link[] = [
47
47
  ]
48
48
 
49
49
  export const legal: Link[] = [
50
- {name: 'Cookie policy', href: '/cookie-policy'},
51
- {name: 'Privacy policy', href: '/privacy-policy'},
50
+ {name: 'Cookie policy', href: paths.cookiePolicy},
51
+ {name: 'Privacy policy', href: paths.privacyPolicy},
52
52
  ]
@@ -1,21 +1,33 @@
1
+ /* Every value here is a COMPLETE url, trailing slash included, because
2
+ * build.format is Astro's default 'directory' — /workflow is emitted as
3
+ * workflow/index.html and only /workflow/ is the address of that file.
4
+ * Written without the slash, all 716 internal links on the site were a 308
5
+ * to the same page with one added; nothing complained because Astro's
6
+ * trailingSlash default is 'ignore'. astro.config.mjs now sets 'always', so
7
+ * dev 404s a slash-less route instead of quietly serving it.
8
+ *
9
+ * Consequence for callers: a path is a PREFIX you append to, never a
10
+ * segment you join with a slash. Write `${paths.insights}${slug}/`, not
11
+ * `${paths.insights}/${slug}` — the latter now yields //. scripts/check-links.mjs
12
+ * fails the build on either mistake. */
1
13
  const services = '/services'
2
14
 
3
15
  export const paths = {
4
16
  home: '/',
5
17
 
6
- devops: `${services}/devops`,
7
- autodesk: `${services}/autodesk`,
8
- hubspot: `${services}/hubspot`,
9
- ecommerce: `${services}/ecommerce`,
10
- automation: `${services}/automation`,
11
- arVr: `${services}/ar-vr`,
18
+ devops: `${services}/devops/`,
19
+ autodesk: `${services}/autodesk/`,
20
+ hubspot: `${services}/hubspot/`,
21
+ ecommerce: `${services}/ecommerce/`,
22
+ automation: `${services}/automation/`,
23
+ arVr: `${services}/ar-vr/`,
12
24
 
13
- workflow: '/workflow',
14
- projects: '/projects',
15
- insights: '/insights',
25
+ workflow: '/workflow/',
26
+ projects: '/projects/',
27
+ insights: '/insights/',
16
28
  contactUs: '/#contact-us',
17
- cookiePolicy: '/cookie-policy',
18
- privacyPolicy: '/privacy-policy',
29
+ cookiePolicy: '/cookie-policy/',
30
+ privacyPolicy: '/privacy-policy/',
19
31
 
20
32
  testimonials: '/#testimonials',
21
33
  strongPoints: '/workflow/#our-strong-point',
@@ -23,3 +35,30 @@ export const paths = {
23
35
  }
24
36
 
25
37
  export const websiteUrl = 'https://www.codecave.it'
38
+
39
+ /* Give an internal href the trailing slash the route table already carries.
40
+ *
41
+ * Links in CMS prose do not come from the table above — an editor writes
42
+ * them, and the privacy policy links the cookie policy as a bare
43
+ * https://www.codecave.it/cookie-policy, one 308 on a page nobody rebuilds.
44
+ * Normalising at render time is the only fix that survives the next edit.
45
+ *
46
+ * Deliberately narrow: internal only, and only where the last segment has no
47
+ * dot, so a linked .pdf keeps its own address. A #fragment or ?query is put
48
+ * back after the slash, since /workflow#x and /workflow/#x are different
49
+ * requests for the same reason. */
50
+ export const internalHref = (href: string): string => {
51
+ if (!href) return href
52
+
53
+ const rest = href.startsWith(websiteUrl)
54
+ ? href.slice(websiteUrl.length)
55
+ : href.startsWith('/') && !href.startsWith('//')
56
+ ? href
57
+ : null
58
+ if (rest === null) return href
59
+
60
+ const [path, tail = ''] = [rest.split(/[#?]/)[0], rest.slice(rest.split(/[#?]/)[0].length)]
61
+ if (!path || path.endsWith('/') || path.split('/').pop()!.includes('.')) return href
62
+
63
+ return href.slice(0, href.length - rest.length) + path + '/' + tail
64
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codecavepro/brand",
3
- "version": "1.4.0",
3
+ "version": "1.6.0",
4
4
  "description": "CODECAVE design system — colour, typography and layout tokens as CSS custom properties and as a typed module.",
5
5
  "license": "Unlicense",
6
6
  "type": "module",