@devicechain/brand 0.14.0-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/LICENSE +201 -0
- package/README.md +108 -0
- package/css/brand.css +55 -0
- package/css/infima.css +32 -0
- package/css/shadcn.css +33 -0
- package/package.json +49 -0
- package/tokens.generated.json +57 -0
- package/tokens.json +100 -0
- package/tools/color.mjs +91 -0
- package/tools/generate.mjs +243 -0
- package/tools/verify.mjs +203 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
6
|
+
|
|
7
|
+
1. Definitions.
|
|
8
|
+
|
|
9
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
10
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
11
|
+
|
|
12
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
13
|
+
the copyright owner that is granting the License.
|
|
14
|
+
|
|
15
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
16
|
+
other entities that control, are controlled by, or are under common
|
|
17
|
+
control with that entity. For the purposes of this definition,
|
|
18
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
19
|
+
direction or management of such entity, whether by contract or
|
|
20
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
21
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
22
|
+
|
|
23
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
24
|
+
exercising permissions granted by this License.
|
|
25
|
+
|
|
26
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
27
|
+
including but not limited to software source code, documentation
|
|
28
|
+
source, and configuration files.
|
|
29
|
+
|
|
30
|
+
"Object" form shall mean any form resulting from mechanical
|
|
31
|
+
transformation or translation of a Source form, including but
|
|
32
|
+
not limited to compiled object code, generated documentation,
|
|
33
|
+
and conversions to other media types.
|
|
34
|
+
|
|
35
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
36
|
+
Object form, made available under the License, as indicated by a
|
|
37
|
+
copyright notice that is included in or attached to the work
|
|
38
|
+
(an example is provided in the Appendix below).
|
|
39
|
+
|
|
40
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
41
|
+
form, that is based on (or derived from) the Work and for which the
|
|
42
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
43
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
44
|
+
of this License, Derivative Works shall not include works that remain
|
|
45
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
46
|
+
the Work and Derivative Works thereof.
|
|
47
|
+
|
|
48
|
+
"Contribution" shall mean any work of authorship, including
|
|
49
|
+
the original version of the Work and any modifications or additions
|
|
50
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
51
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
52
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
53
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
54
|
+
means any form of electronic, verbal, or written communication sent
|
|
55
|
+
to the Licensor or its representatives, including but not limited to
|
|
56
|
+
communication on electronic mailing lists, source code control systems,
|
|
57
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
58
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
59
|
+
excluding communication that is conspicuously marked or otherwise
|
|
60
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
61
|
+
|
|
62
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
63
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
64
|
+
subsequently incorporated within the Work.
|
|
65
|
+
|
|
66
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
67
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
68
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
69
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
70
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
71
|
+
Work and such Derivative Works in Source or Object form.
|
|
72
|
+
|
|
73
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
74
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
75
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
76
|
+
(except as stated in this section) patent license to make, have made,
|
|
77
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
78
|
+
where such license applies only to those patent claims licensable
|
|
79
|
+
by such Contributor that are necessarily infringed by their
|
|
80
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
81
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
82
|
+
institute patent litigation against any entity (including a
|
|
83
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
84
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
85
|
+
or contributory patent infringement, then any patent licenses
|
|
86
|
+
granted to You under this License for that Work shall terminate
|
|
87
|
+
as of the date such litigation is filed.
|
|
88
|
+
|
|
89
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
90
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
91
|
+
modifications, and in Source or Object form, provided that You
|
|
92
|
+
meet the following conditions:
|
|
93
|
+
|
|
94
|
+
(a) You must give any other recipients of the Work or
|
|
95
|
+
Derivative Works a copy of this License; and
|
|
96
|
+
|
|
97
|
+
(b) You must cause any modified files to carry prominent notices
|
|
98
|
+
stating that You changed the files; and
|
|
99
|
+
|
|
100
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
101
|
+
that You distribute, all copyright, patent, trademark, and
|
|
102
|
+
attribution notices from the Source form of the Work,
|
|
103
|
+
excluding those notices that do not pertain to any part of
|
|
104
|
+
the Derivative Works; and
|
|
105
|
+
|
|
106
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
107
|
+
distribution, then any Derivative Works that You distribute must
|
|
108
|
+
include a readable copy of the attribution notices contained
|
|
109
|
+
within such NOTICE file, excluding those notices that do not
|
|
110
|
+
pertain to any part of the Derivative Works, in at least one
|
|
111
|
+
of the following places: within a NOTICE text file distributed
|
|
112
|
+
as part of the Derivative Works; within the Source form or
|
|
113
|
+
documentation, if provided along with the Derivative Works; or,
|
|
114
|
+
within a display generated by the Derivative Works, if and
|
|
115
|
+
wherever such third-party notices normally appear. The contents
|
|
116
|
+
of the NOTICE file are for informational purposes only and
|
|
117
|
+
do not modify the License. You may add Your own attribution
|
|
118
|
+
notices within Derivative Works that You distribute, alongside
|
|
119
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
120
|
+
that such additional attribution notices cannot be construed
|
|
121
|
+
as modifying the License.
|
|
122
|
+
|
|
123
|
+
You may add Your own copyright statement to Your modifications and
|
|
124
|
+
may provide additional or different license terms and conditions
|
|
125
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
126
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
127
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
128
|
+
the conditions stated in this License.
|
|
129
|
+
|
|
130
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
131
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
132
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
133
|
+
this License, without any additional terms or conditions.
|
|
134
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
135
|
+
the terms of any separate license agreement you may have executed
|
|
136
|
+
with Licensor regarding such Contributions.
|
|
137
|
+
|
|
138
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
139
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
140
|
+
except as required for reasonable and customary use in describing the
|
|
141
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
142
|
+
|
|
143
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
144
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
145
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
146
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
147
|
+
implied, including, without limitation, any warranties or conditions
|
|
148
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
149
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
150
|
+
appropriateness of using or redistributing the Work and assume any
|
|
151
|
+
risks associated with Your exercise of permissions under this License.
|
|
152
|
+
|
|
153
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
154
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
155
|
+
unless required by applicable law (such as deliberate and grossly
|
|
156
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
157
|
+
liable to You for damages, including any direct, indirect, special,
|
|
158
|
+
incidental, or consequential damages of any character arising as a
|
|
159
|
+
result of this License or out of the use or inability to use the
|
|
160
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
161
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
162
|
+
other commercial damages or losses), even if such Contributor
|
|
163
|
+
has been advised of the possibility of such damages.
|
|
164
|
+
|
|
165
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
166
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
167
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
168
|
+
or other liability obligations and/or rights consistent with this
|
|
169
|
+
License. However, in accepting such obligations, You may act only
|
|
170
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
171
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
172
|
+
defend, and hold each Contributor harmless for any liability
|
|
173
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
174
|
+
of your accepting any such warranty or additional liability.
|
|
175
|
+
|
|
176
|
+
END OF TERMS AND CONDITIONS
|
|
177
|
+
|
|
178
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
179
|
+
|
|
180
|
+
To apply the Apache License to your work, attach the following
|
|
181
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
182
|
+
replaced with your own identifying information. (Don't include
|
|
183
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
184
|
+
comment syntax for the file format. We also recommend that a
|
|
185
|
+
file or class name and description of purpose be included on the
|
|
186
|
+
same "printed page" as the copyright notice for easier
|
|
187
|
+
identification within third-party archives.
|
|
188
|
+
|
|
189
|
+
Copyright The DeviceChain Authors
|
|
190
|
+
|
|
191
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
192
|
+
you may not use this file except in compliance with the License.
|
|
193
|
+
You may obtain a copy of the License at
|
|
194
|
+
|
|
195
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
196
|
+
|
|
197
|
+
Unless required by applicable law or agreed to in writing, software
|
|
198
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
199
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
200
|
+
See the License for the specific language governing permissions and
|
|
201
|
+
limitations under the License.
|
package/README.md
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
Copyright The DeviceChain Authors
|
|
3
|
+
SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
-->
|
|
5
|
+
|
|
6
|
+
# @devicechain/brand
|
|
7
|
+
|
|
8
|
+
The canonical DeviceChain brand values, and the generated stylesheets each
|
|
9
|
+
consumer actually needs.
|
|
10
|
+
|
|
11
|
+
`tokens.json` is the source of truth. Everything in `css/` and
|
|
12
|
+
`tokens.generated.json` is generated from it and carries a do-not-hand-edit
|
|
13
|
+
header.
|
|
14
|
+
|
|
15
|
+
## Using it
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install @devicechain/brand
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
```css
|
|
22
|
+
/* whichever vocabulary your app speaks — see the table below */
|
|
23
|
+
@import '@devicechain/brand/css/shadcn.css';
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
import tokens from '@devicechain/brand/tokens';
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
There is no build step and no JavaScript to run: the stylesheets and the
|
|
31
|
+
generated token JSON are shipped as-is.
|
|
32
|
+
|
|
33
|
+
## Working on it
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npm run generate -w @devicechain/brand # regenerate after editing tokens.json
|
|
37
|
+
npm run check:generated -w @devicechain/brand # fail if the committed output is stale
|
|
38
|
+
npm run verify -w @devicechain/brand # report consumers that disagree
|
|
39
|
+
|
|
40
|
+
# the marketing site lives in a separate repo, so point at it explicitly
|
|
41
|
+
DC_WEBSITE=../devicechain-website npm run verify -w @devicechain/brand
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Why three stylesheets and not one
|
|
45
|
+
|
|
46
|
+
The three consumers do not share a vocabulary, and a single stylesheet cannot
|
|
47
|
+
serve them:
|
|
48
|
+
|
|
49
|
+
| consumer | file | vocabulary | value format |
|
|
50
|
+
| --- | --- | --- | --- |
|
|
51
|
+
| marketing site | `css/brand.css` | `--brand`, `--ink`, `--bg`, accents | hex |
|
|
52
|
+
| console | `css/shadcn.css` | `--primary`, `--ring`, `--sidebar-*` | **bare** `H S% L%` triples |
|
|
53
|
+
| docs | `css/infima.css` | `--ifm-color-primary*` | hex, plus a five-step ramp |
|
|
54
|
+
|
|
55
|
+
Two details that are easy to get wrong:
|
|
56
|
+
|
|
57
|
+
- The console's triples have **no `hsl()` wrapper**. shadcn composes them as
|
|
58
|
+
`hsl(var(--primary) / <alpha>)`, so wrapping them breaks every alpha variant.
|
|
59
|
+
- Infima wants a five-step ramp either side of primary. It is generated from the
|
|
60
|
+
single canonical value so the steps cannot drift out of order, which is the
|
|
61
|
+
usual failure mode of a hand-written ramp.
|
|
62
|
+
|
|
63
|
+
`css/shadcn.css` and `css/infima.css` deliberately cover **brand-derived tokens
|
|
64
|
+
only**. The console still owns the rest of its shadcn scale (card, popover,
|
|
65
|
+
`chart-*`, `sidebar-*` surfaces) and the docs site still owns its own
|
|
66
|
+
background theming.
|
|
67
|
+
|
|
68
|
+
## Why the values are stored as hex
|
|
69
|
+
|
|
70
|
+
The brandmark artwork is hex (`branding/logo.svg`), so hex is the origin and
|
|
71
|
+
every other form is derived. This is not cosmetic. The console previously
|
|
72
|
+
declared the brand blue as the rounded triple `197 71% 42%`, which renders
|
|
73
|
+
`#1f8cb7` — **not** the `#208cb7` in the artwork. One channel out of 255, invisible
|
|
74
|
+
to the eye, and still two different colours claiming to be the same one.
|
|
75
|
+
Deriving removes that entire class of mismatch.
|
|
76
|
+
|
|
77
|
+
## The one rule
|
|
78
|
+
|
|
79
|
+
`core.primary` is the only colour that ever means *interactive*. The four
|
|
80
|
+
accents (`aqua` → `violet` → `magenta` → `amber`, in pipeline order: ingest,
|
|
81
|
+
detect, react, command) identify an **area**. They never mark a link, a button
|
|
82
|
+
or a control. Break that and a page stops being navigable.
|
|
83
|
+
|
|
84
|
+
## What `verify` is for, and what it is not
|
|
85
|
+
|
|
86
|
+
"Keep it in step with the console" was a comment in three files, not a
|
|
87
|
+
guarantee — and it had already failed twice by the time this package was
|
|
88
|
+
written. `verify` turns that comment into something a CI job can enforce.
|
|
89
|
+
|
|
90
|
+
It is deliberately **read-only**. It prints what disagrees and exits non-zero;
|
|
91
|
+
it never rewrites a consumer. Changing a shipped colour is a visual decision,
|
|
92
|
+
so migrate consumers one at a time and look at the result. Do not bulk-rewrite
|
|
93
|
+
to silence the script.
|
|
94
|
+
|
|
95
|
+
## Migration status
|
|
96
|
+
|
|
97
|
+
No consumer has been migrated yet — this package is currently the source of
|
|
98
|
+
truth plus the drift alarm. Known disagreements at time of writing:
|
|
99
|
+
|
|
100
|
+
- **console** `--primary` (light and dark) — rounded triples, as described
|
|
101
|
+
above. Adopting the derived values is a no-op to the eye.
|
|
102
|
+
- **marketing site** `--brand-bright` — had drifted to `#3aa9d4`
|
|
103
|
+
(`hsl(197 64% 53%)`), a third value that neither the console nor the docs
|
|
104
|
+
used. The console and docs both use `#24a3d6` (`hsl(197 71% 49%)`), and the
|
|
105
|
+
console is the documented source of truth, so it wins here. Applying it is a
|
|
106
|
+
**visible** change to the site's hover states and hero eyebrow and should be
|
|
107
|
+
reviewed on its own.
|
|
108
|
+
- **docs** — already agrees on both light and dark.
|
package/css/brand.css
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/* Copyright The DeviceChain Authors */
|
|
2
|
+
/* SPDX-License-Identifier: Apache-2.0 */
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* GENERATED by tools/generate.mjs from tokens.json — DO NOT HAND-EDIT.
|
|
6
|
+
*
|
|
7
|
+
* Marketing + static-site vocabulary. Consumed by the public website and by
|
|
8
|
+
* anything with no bundler: plain custom properties, hex values, no build step.
|
|
9
|
+
*
|
|
10
|
+
* This is the one output that is COPIED INTO ANOTHER REPOSITORY, so it names
|
|
11
|
+
* its source in full: frontend/packages/brand/tokens.json in the devicechain
|
|
12
|
+
* monorepo. Editing the copy is how the marketing site drifted onto a blue
|
|
13
|
+
* neither the console nor the docs used. Change tokens.json and re-sync.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
:root {
|
|
17
|
+
/* Core brand — sampled from the brandmark (branding/logo.svg). */
|
|
18
|
+
--brand: #208cb7;
|
|
19
|
+
--brand-bright: #24a3d6;
|
|
20
|
+
--brand-deep: #1f425e;
|
|
21
|
+
--brand-pale: #9aceec;
|
|
22
|
+
|
|
23
|
+
/* Surfaces + text. */
|
|
24
|
+
--bg: #0b1620;
|
|
25
|
+
/* LOAD-BEARING: must equal the hex-field renderer's gl.clearColor. If they
|
|
26
|
+
differ, distant fogged tiles read as a dark silhouette against a lighter
|
|
27
|
+
sky instead of dissolving into it. */
|
|
28
|
+
--bg-deep: #060b11;
|
|
29
|
+
--ink: #e8f1f6;
|
|
30
|
+
--ink-2: #b6ccd8;
|
|
31
|
+
--muted: #8aa6b6;
|
|
32
|
+
|
|
33
|
+
/* Derived surface treatments. */
|
|
34
|
+
--panel: rgba(255, 255, 255, 0.04);
|
|
35
|
+
--border: rgba(122, 183, 217, 0.18);
|
|
36
|
+
--rule: rgba(122, 183, 217, 0.16);
|
|
37
|
+
--rule-strong: rgba(122, 183, 217, 0.34);
|
|
38
|
+
--glow-strong: rgba(32, 140, 183, 0.25);
|
|
39
|
+
--glow-soft: rgba(32, 140, 183, 0.12);
|
|
40
|
+
|
|
41
|
+
/* Area accents. NEVER used for anything interactive — --brand alone
|
|
42
|
+
carries that meaning. Order is pipeline order. */
|
|
43
|
+
/* Ingest and edge. */
|
|
44
|
+
--aqua: #2fd6d0;
|
|
45
|
+
/* Detect. */
|
|
46
|
+
--violet: #7c6cf5;
|
|
47
|
+
/* React. */
|
|
48
|
+
--magenta: #e8489b;
|
|
49
|
+
/* Command. */
|
|
50
|
+
--amber: #f5a524;
|
|
51
|
+
|
|
52
|
+
/* Type stacks. */
|
|
53
|
+
--sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
54
|
+
--mono: ui-monospace, "SF Mono", "DejaVu Sans Mono", "Liberation Mono", Menlo, Consolas, monospace;
|
|
55
|
+
}
|
package/css/infima.css
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/* Copyright The DeviceChain Authors */
|
|
2
|
+
/* SPDX-License-Identifier: Apache-2.0 */
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* GENERATED by tools/generate.mjs from tokens.json — DO NOT HAND-EDIT.
|
|
6
|
+
*
|
|
7
|
+
* Docs vocabulary (Docusaurus/Infima). Infima needs a five-step ramp either
|
|
8
|
+
* side of primary; it is generated from the single canonical value so the
|
|
9
|
+
* steps cannot drift out of order.
|
|
10
|
+
*
|
|
11
|
+
* Brand colours only — the docs site owns its own background/surface theming.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
:root {
|
|
15
|
+
--ifm-color-primary: #208cb7;
|
|
16
|
+
--ifm-color-primary-dark: #1d7ea5;
|
|
17
|
+
--ifm-color-primary-darker: #1b779c;
|
|
18
|
+
--ifm-color-primary-darkest: #166280;
|
|
19
|
+
--ifm-color-primary-light: #239ac9;
|
|
20
|
+
--ifm-color-primary-lighter: #25a1d3;
|
|
21
|
+
--ifm-color-primary-lightest: #3bafdd;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
[data-theme='dark'] {
|
|
25
|
+
--ifm-color-primary: #24a3d6;
|
|
26
|
+
--ifm-color-primary-dark: #2093c1;
|
|
27
|
+
--ifm-color-primary-darker: #1f8ab6;
|
|
28
|
+
--ifm-color-primary-darkest: #197296;
|
|
29
|
+
--ifm-color-primary-light: #36addd;
|
|
30
|
+
--ifm-color-primary-lighter: #40b2df;
|
|
31
|
+
--ifm-color-primary-lightest: #61bfe4;
|
|
32
|
+
}
|
package/css/shadcn.css
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/* Copyright The DeviceChain Authors */
|
|
2
|
+
/* SPDX-License-Identifier: Apache-2.0 */
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* GENERATED by tools/generate.mjs from tokens.json — DO NOT HAND-EDIT.
|
|
6
|
+
*
|
|
7
|
+
* Console vocabulary. Values are BARE `H S% L%` triples with no hsl() wrapper,
|
|
8
|
+
* because shadcn composes them as `hsl(var(--primary) / <alpha>)` — wrapping
|
|
9
|
+
* them here would silently break every alpha variant.
|
|
10
|
+
*
|
|
11
|
+
* Scope: every token whose value is DETERMINED BY the brand blue — the blue
|
|
12
|
+
* itself wherever shadcn names it, plus the foreground that has to read on
|
|
13
|
+
* top of it. A consumer keeps ownership of everything else in its scale
|
|
14
|
+
* (background, card, popover, chart-*, and the non-primary sidebar-*).
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
:root {
|
|
18
|
+
--primary: 197.1 70.2% 42.2%;
|
|
19
|
+
--primary-foreground: 0 0% 100%;
|
|
20
|
+
--ring: 197.1 70.2% 42.2%;
|
|
21
|
+
--sidebar-primary: 197.1 70.2% 42.2%;
|
|
22
|
+
--sidebar-primary-foreground: 0 0% 100%;
|
|
23
|
+
--sidebar-ring: 197.1 70.2% 42.2%;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.dark {
|
|
27
|
+
--primary: 197.2 71.2% 49%;
|
|
28
|
+
--primary-foreground: 0 0% 100%;
|
|
29
|
+
--ring: 197.2 71.2% 49%;
|
|
30
|
+
--sidebar-primary: 197.2 71.2% 49%;
|
|
31
|
+
--sidebar-primary-foreground: 0 0% 100%;
|
|
32
|
+
--sidebar-ring: 197.2 71.2% 49%;
|
|
33
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@devicechain/brand",
|
|
3
|
+
"version": "0.14.0-0",
|
|
4
|
+
"description": "Canonical DeviceChain brand tokens, plus the generated per-consumer stylesheets.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"devicechain",
|
|
7
|
+
"design-tokens",
|
|
8
|
+
"css",
|
|
9
|
+
"theme"
|
|
10
|
+
],
|
|
11
|
+
"license": "Apache-2.0",
|
|
12
|
+
"homepage": "https://devicechain.io",
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git+https://github.com/devicechain-io/devicechain.git",
|
|
16
|
+
"directory": "frontend/packages/brand"
|
|
17
|
+
},
|
|
18
|
+
"bugs": {
|
|
19
|
+
"url": "https://github.com/devicechain-io/devicechain/issues"
|
|
20
|
+
},
|
|
21
|
+
"type": "module",
|
|
22
|
+
"sideEffects": [
|
|
23
|
+
"./css/*.css"
|
|
24
|
+
],
|
|
25
|
+
"exports": {
|
|
26
|
+
"./tokens.json": "./tokens.json",
|
|
27
|
+
"./tokens": "./tokens.generated.json",
|
|
28
|
+
"./css/brand.css": "./css/brand.css",
|
|
29
|
+
"./css/shadcn.css": "./css/shadcn.css",
|
|
30
|
+
"./css/infima.css": "./css/infima.css"
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"css",
|
|
34
|
+
"tokens.json",
|
|
35
|
+
"tokens.generated.json",
|
|
36
|
+
"tools",
|
|
37
|
+
"README.md",
|
|
38
|
+
"LICENSE"
|
|
39
|
+
],
|
|
40
|
+
"scripts": {
|
|
41
|
+
"generate": "node tools/generate.mjs",
|
|
42
|
+
"check:generated": "node tools/generate.mjs --check",
|
|
43
|
+
"verify": "node tools/verify.mjs",
|
|
44
|
+
"build": "node tools/generate.mjs --check"
|
|
45
|
+
},
|
|
46
|
+
"publishConfig": {
|
|
47
|
+
"access": "public"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"flat": {
|
|
3
|
+
"core.primary": "#208cb7",
|
|
4
|
+
"core.primaryBright": "#24a3d6",
|
|
5
|
+
"core.primaryDeep": "#1f425e",
|
|
6
|
+
"core.primaryPale": "#9aceec",
|
|
7
|
+
"surface.bg": "#0b1620",
|
|
8
|
+
"surface.bgDeep": "#060b11",
|
|
9
|
+
"surface.ink": "#e8f1f6",
|
|
10
|
+
"surface.ink2": "#b6ccd8",
|
|
11
|
+
"surface.muted": "#8aa6b6",
|
|
12
|
+
"accents.aqua": "#2fd6d0",
|
|
13
|
+
"accents.violet": "#7c6cf5",
|
|
14
|
+
"accents.magenta": "#e8489b",
|
|
15
|
+
"accents.amber": "#f5a524",
|
|
16
|
+
"type.sans": "ui-sans-serif, system-ui, -apple-system, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif",
|
|
17
|
+
"type.mono": "ui-monospace, \"SF Mono\", \"DejaVu Sans Mono\", \"Liberation Mono\", Menlo, Consolas, monospace"
|
|
18
|
+
},
|
|
19
|
+
"derived": {
|
|
20
|
+
"primary": {
|
|
21
|
+
"hex": "#208cb7",
|
|
22
|
+
"hsl": {
|
|
23
|
+
"h": 197.1,
|
|
24
|
+
"s": 70.2,
|
|
25
|
+
"l": 42.2
|
|
26
|
+
},
|
|
27
|
+
"shadcn": "197.1 70.2% 42.2%"
|
|
28
|
+
},
|
|
29
|
+
"primaryBright": {
|
|
30
|
+
"hex": "#24a3d6",
|
|
31
|
+
"hsl": {
|
|
32
|
+
"h": 197.2,
|
|
33
|
+
"s": 71.2,
|
|
34
|
+
"l": 49
|
|
35
|
+
},
|
|
36
|
+
"shadcn": "197.2 71.2% 49%"
|
|
37
|
+
},
|
|
38
|
+
"primaryDeep": {
|
|
39
|
+
"hex": "#1f425e",
|
|
40
|
+
"hsl": {
|
|
41
|
+
"h": 206.7,
|
|
42
|
+
"s": 50.4,
|
|
43
|
+
"l": 24.5
|
|
44
|
+
},
|
|
45
|
+
"shadcn": "206.7 50.4% 24.5%"
|
|
46
|
+
},
|
|
47
|
+
"primaryPale": {
|
|
48
|
+
"hex": "#9aceec",
|
|
49
|
+
"hsl": {
|
|
50
|
+
"h": 202,
|
|
51
|
+
"s": 68.3,
|
|
52
|
+
"l": 76.5
|
|
53
|
+
},
|
|
54
|
+
"shadcn": "202 68.3% 76.5%"
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
package/tokens.json
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$license": "Copyright The DeviceChain Authors; SPDX-License-Identifier: Apache-2.0",
|
|
3
|
+
"$about": [
|
|
4
|
+
"THE canonical DeviceChain brand values. Every other representation is",
|
|
5
|
+
"GENERATED from this file — see tools/generate.mjs and the css/ directory.",
|
|
6
|
+
"Edit here, run `npm run generate -w @devicechain/brand`, commit the result.",
|
|
7
|
+
"",
|
|
8
|
+
"Values are stored as hex because the brandmark artwork is hex",
|
|
9
|
+
"(branding/logo.svg). HSL forms are DERIVED, never hand-written: the console",
|
|
10
|
+
"previously declared the brand blue as the rounded triple `197 71% 42%`,",
|
|
11
|
+
"which renders #1f8cb7 and is therefore not the same colour as the #208cb7",
|
|
12
|
+
"in the artwork. Deriving removes that class of mismatch entirely."
|
|
13
|
+
],
|
|
14
|
+
"core": {
|
|
15
|
+
"primary": {
|
|
16
|
+
"value": "#208cb7",
|
|
17
|
+
"role": "Brand blue. The logo's \"CHAIN\" wordmark and cube. THE interactive colour — the only one that ever means \"you can click this\"."
|
|
18
|
+
},
|
|
19
|
+
"primaryBright": {
|
|
20
|
+
"value": "#24a3d6",
|
|
21
|
+
"role": "Primary lifted for dark grounds and hover states.",
|
|
22
|
+
"$note": "Reconciles a real three-way divergence. The console (dark `hsl(197 71% 49%)`) and the docs (dark `#24a3d6`) already agreed on this value; the marketing site had drifted to #3aa9d4 = hsl(197 64% 53%), which is visibly lighter and less saturated. Console is the documented source of truth and carried the majority, so it wins. Applying this to the website is a VISIBLE change to hover states and the hero eyebrow and must be reviewed on its own."
|
|
23
|
+
},
|
|
24
|
+
"primaryDeep": {
|
|
25
|
+
"value": "#1f425e",
|
|
26
|
+
"role": "Deepest cube facet. Navy, used for grounds rather than text."
|
|
27
|
+
},
|
|
28
|
+
"primaryPale": {
|
|
29
|
+
"value": "#9aceec",
|
|
30
|
+
"role": "Lightest cube facet. Highlights and hairlines."
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"surface": {
|
|
34
|
+
"bg": {
|
|
35
|
+
"value": "#0b1620",
|
|
36
|
+
"role": "Default dark canvas — the \"devicechain.io treatment\" ground."
|
|
37
|
+
},
|
|
38
|
+
"bgDeep": {
|
|
39
|
+
"value": "#060b11",
|
|
40
|
+
"role": "Deeper canvas for pages carrying the hex field.",
|
|
41
|
+
"$note": "LOAD-BEARING: must equal the hex-field renderer's gl.clearColor. If they differ, distant fogged tiles read as a dark silhouette against a lighter sky instead of dissolving into it."
|
|
42
|
+
},
|
|
43
|
+
"ink": {
|
|
44
|
+
"value": "#e8f1f6",
|
|
45
|
+
"role": "Primary text on dark."
|
|
46
|
+
},
|
|
47
|
+
"ink2": {
|
|
48
|
+
"value": "#b6ccd8",
|
|
49
|
+
"role": "Secondary prose — ledes and section body."
|
|
50
|
+
},
|
|
51
|
+
"muted": {
|
|
52
|
+
"value": "#8aa6b6",
|
|
53
|
+
"role": "Tertiary text, captions, metadata."
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
"accents": {
|
|
57
|
+
"$about": [
|
|
58
|
+
"The expanded graphical language. The core palette above is a single hue —",
|
|
59
|
+
"every swatch sits between 197 and 207 — which is why the brand reads as",
|
|
60
|
+
"consistent but flat. These five stops walk monotonically THROUGH the",
|
|
61
|
+
"anchor blue rather than away from it, so each product area gets its own",
|
|
62
|
+
"identity without any of them fighting the brand.",
|
|
63
|
+
"",
|
|
64
|
+
"ONE RULE, and it is the whole system: core.primary is the only colour that",
|
|
65
|
+
"ever means \"interactive\". An accent identifies an AREA. It never marks a",
|
|
66
|
+
"link, a button or a control. Break that and the page stops being",
|
|
67
|
+
"navigable.",
|
|
68
|
+
"",
|
|
69
|
+
"Order is pipeline order: ingest -> detect -> react -> command."
|
|
70
|
+
],
|
|
71
|
+
"aqua": {
|
|
72
|
+
"value": "#2fd6d0",
|
|
73
|
+
"role": "Ingest and edge."
|
|
74
|
+
},
|
|
75
|
+
"violet": {
|
|
76
|
+
"value": "#7c6cf5",
|
|
77
|
+
"role": "Detect."
|
|
78
|
+
},
|
|
79
|
+
"magenta": {
|
|
80
|
+
"value": "#e8489b",
|
|
81
|
+
"role": "React."
|
|
82
|
+
},
|
|
83
|
+
"amber": {
|
|
84
|
+
"value": "#f5a524",
|
|
85
|
+
"role": "Command.",
|
|
86
|
+
"$note": "Also the deliberate antidote to reading a product called DeviceChain as web3 — warm is the one thing that palette never is."
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
"type": {
|
|
90
|
+
"sans": {
|
|
91
|
+
"value": "ui-sans-serif, system-ui, -apple-system, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif",
|
|
92
|
+
"role": "UI and prose.",
|
|
93
|
+
"$note": "Placeholder stack. A real display/body pairing is still an open brand decision; the hero's headline sizing is currently tuned against whatever system-ui resolves to and will need re-tuning when a webfont lands."
|
|
94
|
+
},
|
|
95
|
+
"mono": {
|
|
96
|
+
"value": "ui-monospace, \"SF Mono\", \"DejaVu Sans Mono\", \"Liberation Mono\", Menlo, Consolas, monospace",
|
|
97
|
+
"role": "Eyebrows, labels, code, tabular data."
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
package/tools/color.mjs
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// Copyright The DeviceChain Authors
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
// Colour conversion for the token generator. Hand-rolled rather than pulling a
|
|
5
|
+
// dependency: this package must stay installable with zero transitive deps so it
|
|
6
|
+
// can be consumed by the docs build, the console build and (eventually) a static
|
|
7
|
+
// site with no bundler at all.
|
|
8
|
+
|
|
9
|
+
/** @param {string} hex `#rrggbb` */
|
|
10
|
+
export function hexToRgb(hex) {
|
|
11
|
+
const h = hex.trim().replace(/^#/, '');
|
|
12
|
+
if (!/^[0-9a-fA-F]{6}$/.test(h)) {
|
|
13
|
+
throw new Error(`not a 6-digit hex colour: ${hex}`);
|
|
14
|
+
}
|
|
15
|
+
return [
|
|
16
|
+
parseInt(h.slice(0, 2), 16),
|
|
17
|
+
parseInt(h.slice(2, 4), 16),
|
|
18
|
+
parseInt(h.slice(4, 6), 16),
|
|
19
|
+
];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* sRGB -> HSL. Returns hue in degrees, saturation and lightness as percentages.
|
|
24
|
+
*
|
|
25
|
+
* Rounded to ONE decimal place deliberately. Full precision would emit values
|
|
26
|
+
* like 197.14285714285714 that churn the generated files on every regeneration
|
|
27
|
+
* for no visible benefit; integers would reintroduce exactly the rounding drift
|
|
28
|
+
* this package exists to remove (`197 71% 42%` renders #1f8cb7, not #208cb7).
|
|
29
|
+
*/
|
|
30
|
+
export function hexToHsl(hex) {
|
|
31
|
+
const [r8, g8, b8] = hexToRgb(hex);
|
|
32
|
+
const r = r8 / 255;
|
|
33
|
+
const g = g8 / 255;
|
|
34
|
+
const b = b8 / 255;
|
|
35
|
+
const max = Math.max(r, g, b);
|
|
36
|
+
const min = Math.min(r, g, b);
|
|
37
|
+
const l = (max + min) / 2;
|
|
38
|
+
let h = 0;
|
|
39
|
+
let s = 0;
|
|
40
|
+
if (max !== min) {
|
|
41
|
+
const d = max - min;
|
|
42
|
+
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
43
|
+
if (max === r) h = ((g - b) / d + (g < b ? 6 : 0)) / 6;
|
|
44
|
+
else if (max === g) h = ((b - r) / d + 2) / 6;
|
|
45
|
+
else h = ((r - g) / d + 4) / 6;
|
|
46
|
+
}
|
|
47
|
+
const round1 = (n) => Math.round(n * 10) / 10;
|
|
48
|
+
return { h: round1(h * 360), s: round1(s * 100), l: round1(l * 100) };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The bare `H S% L%` triple shadcn/Tailwind expects, e.g. `197.1 70.2% 42.2%`.
|
|
53
|
+
* Note the absence of `hsl(...)` — shadcn composes these into
|
|
54
|
+
* `hsl(var(--primary) / <alpha>)`, so wrapping them breaks alpha support.
|
|
55
|
+
*/
|
|
56
|
+
export function hexToShadcnTriple(hex) {
|
|
57
|
+
const { h, s, l } = hexToHsl(hex);
|
|
58
|
+
return `${h} ${s}% ${l}%`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** HSL (deg, %, %) -> `#rrggbb`. */
|
|
62
|
+
export function hslToHex(h, s, l) {
|
|
63
|
+
const sn = s / 100;
|
|
64
|
+
const ln = l / 100;
|
|
65
|
+
const k = (n) => (n + h / 30) % 12;
|
|
66
|
+
const a = sn * Math.min(ln, 1 - ln);
|
|
67
|
+
const f = (n) => ln - a * Math.max(-1, Math.min(k(n) - 3, Math.min(9 - k(n), 1)));
|
|
68
|
+
const to255 = (x) => Math.round(x * 255);
|
|
69
|
+
return (
|
|
70
|
+
'#' +
|
|
71
|
+
[to255(f(0)), to255(f(8)), to255(f(4))]
|
|
72
|
+
.map((c) => c.toString(16).padStart(2, '0'))
|
|
73
|
+
.join('')
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Scale a colour's LIGHTNESS by `factor`, preserving hue and saturation.
|
|
79
|
+
*
|
|
80
|
+
* This is how Infima's primary ramp is built, and getting it wrong is not
|
|
81
|
+
* subtle. The obvious implementation — linearly mixing toward white or black in
|
|
82
|
+
* RGB — adds white to all three channels equally, which DESATURATES. Measured
|
|
83
|
+
* against the docs site's existing ramp, a mix-to-white `lightest` came out at
|
|
84
|
+
* 51.5% saturation against the correct 71.1%: a visibly washed-out, greyer blue.
|
|
85
|
+
* Scaling lightness in HSL keeps the hue and the saturation exactly where they
|
|
86
|
+
* were and only moves the value up or down the ramp.
|
|
87
|
+
*/
|
|
88
|
+
export function scaleLightness(hex, factor) {
|
|
89
|
+
const { h, s, l } = hexToHsl(hex);
|
|
90
|
+
return hslToHex(h, s, Math.max(0, Math.min(100, l * factor)));
|
|
91
|
+
}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
// Copyright The DeviceChain Authors
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
// Generate every consumer-facing representation of the brand from tokens.json.
|
|
5
|
+
//
|
|
6
|
+
// node tools/generate.mjs # write css/*
|
|
7
|
+
// node tools/generate.mjs --check # fail if anything on disk is stale
|
|
8
|
+
//
|
|
9
|
+
// Three outputs, because the three consumers do NOT share a vocabulary and a
|
|
10
|
+
// single stylesheet cannot serve them:
|
|
11
|
+
//
|
|
12
|
+
// css/brand.css marketing/static: --brand, --ink, --bg, accents (hex)
|
|
13
|
+
// css/shadcn.css console: --primary, --foreground, ... (BARE HSL
|
|
14
|
+
// triples, which is what Tailwind needs)
|
|
15
|
+
// css/infima.css docs: --ifm-color-primary* (hex, with the
|
|
16
|
+
// light/dark ramp Infima expects)
|
|
17
|
+
//
|
|
18
|
+
// Discovering that was the main design constraint: the console declares the
|
|
19
|
+
// brand blue as `197 71% 42%` while everyone else writes `#208cb7`, and Infima
|
|
20
|
+
// wants a five-step ramp nobody else has. Generating per-vocabulary is what
|
|
21
|
+
// makes one source of truth actually possible.
|
|
22
|
+
|
|
23
|
+
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
24
|
+
import { dirname, join } from 'node:path';
|
|
25
|
+
import { fileURLToPath } from 'node:url';
|
|
26
|
+
import { hexToHsl, hexToShadcnTriple, scaleLightness } from './color.mjs';
|
|
27
|
+
|
|
28
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
29
|
+
const ROOT = join(HERE, '..');
|
|
30
|
+
const tokens = JSON.parse(readFileSync(join(ROOT, 'tokens.json'), 'utf8'));
|
|
31
|
+
|
|
32
|
+
const v = (group, key) => tokens[group][key].value;
|
|
33
|
+
const HEADER = (what) =>
|
|
34
|
+
`/* Copyright The DeviceChain Authors */\n` +
|
|
35
|
+
`/* SPDX-License-Identifier: Apache-2.0 */\n\n` +
|
|
36
|
+
`/*\n * GENERATED by tools/generate.mjs from tokens.json — DO NOT HAND-EDIT.\n` +
|
|
37
|
+
` *\n * ${what}\n */\n\n`;
|
|
38
|
+
|
|
39
|
+
/* ── Marketing / static vocabulary ─────────────────────────────────────── */
|
|
40
|
+
|
|
41
|
+
function brandCss() {
|
|
42
|
+
const { core, surface, accents, type } = tokens;
|
|
43
|
+
// Notes are prose and some run long. Wrap them rather than emitting a 140-col
|
|
44
|
+
// line: this is the one generated file that gets COPIED INTO ANOTHER REPO and
|
|
45
|
+
// read there by someone with no access to tokens.json, so the comment is the
|
|
46
|
+
// only explanation they get.
|
|
47
|
+
const wrap = (text, indent) => {
|
|
48
|
+
const width = 76 - indent.length;
|
|
49
|
+
const lines = [];
|
|
50
|
+
let cur = '';
|
|
51
|
+
for (const w of text.split(/\s+/)) {
|
|
52
|
+
if (cur && (cur + ' ' + w).length > width) {
|
|
53
|
+
lines.push(cur);
|
|
54
|
+
cur = w;
|
|
55
|
+
} else cur = cur ? cur + ' ' + w : w;
|
|
56
|
+
}
|
|
57
|
+
if (cur) lines.push(cur);
|
|
58
|
+
return lines.map((l, i) => `${indent}${i === 0 ? '/* ' : ' '}${l}`).join('\n') + ' */\n';
|
|
59
|
+
};
|
|
60
|
+
const line = (name, value, note) =>
|
|
61
|
+
(note ? wrap(note, ' ') : '') + ` --${name}: ${value};\n`;
|
|
62
|
+
let out = HEADER(
|
|
63
|
+
'Marketing + static-site vocabulary. Consumed by the public website and by\n' +
|
|
64
|
+
' * anything with no bundler: plain custom properties, hex values, no build step.\n' +
|
|
65
|
+
' *\n' +
|
|
66
|
+
' * This is the one output that is COPIED INTO ANOTHER REPOSITORY, so it names\n' +
|
|
67
|
+
' * its source in full: frontend/packages/brand/tokens.json in the devicechain\n' +
|
|
68
|
+
' * monorepo. Editing the copy is how the marketing site drifted onto a blue\n' +
|
|
69
|
+
' * neither the console nor the docs used. Change tokens.json and re-sync.'
|
|
70
|
+
);
|
|
71
|
+
out += ':root {\n';
|
|
72
|
+
out += ' /* Core brand — sampled from the brandmark (branding/logo.svg). */\n';
|
|
73
|
+
out += line('brand', core.primary.value);
|
|
74
|
+
out += line('brand-bright', core.primaryBright.value);
|
|
75
|
+
out += line('brand-deep', core.primaryDeep.value);
|
|
76
|
+
out += line('brand-pale', core.primaryPale.value);
|
|
77
|
+
out += '\n /* Surfaces + text. */\n';
|
|
78
|
+
out += line('bg', surface.bg.value);
|
|
79
|
+
out += line('bg-deep', surface.bgDeep.value, surface.bgDeep.$note);
|
|
80
|
+
out += line('ink', surface.ink.value);
|
|
81
|
+
out += line('ink-2', surface.ink2.value);
|
|
82
|
+
out += line('muted', surface.muted.value);
|
|
83
|
+
out += '\n /* Derived surface treatments. */\n';
|
|
84
|
+
out += line('panel', 'rgba(255, 255, 255, 0.04)');
|
|
85
|
+
out += line('border', 'rgba(122, 183, 217, 0.18)');
|
|
86
|
+
out += line('rule', 'rgba(122, 183, 217, 0.16)');
|
|
87
|
+
out += line('rule-strong', 'rgba(122, 183, 217, 0.34)');
|
|
88
|
+
out += line('glow-strong', 'rgba(32, 140, 183, 0.25)');
|
|
89
|
+
out += line('glow-soft', 'rgba(32, 140, 183, 0.12)');
|
|
90
|
+
out +=
|
|
91
|
+
'\n /* Area accents. NEVER used for anything interactive — --brand alone\n' +
|
|
92
|
+
' carries that meaning. Order is pipeline order. */\n';
|
|
93
|
+
for (const k of ['aqua', 'violet', 'magenta', 'amber']) {
|
|
94
|
+
out += line(k, accents[k].value, accents[k].role);
|
|
95
|
+
}
|
|
96
|
+
out += '\n /* Type stacks. */\n';
|
|
97
|
+
out += line('sans', type.sans.value);
|
|
98
|
+
out += line('mono', type.mono.value);
|
|
99
|
+
out += '}\n';
|
|
100
|
+
return out;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/* ── Console (shadcn / Tailwind) vocabulary ────────────────────────────── */
|
|
104
|
+
|
|
105
|
+
function shadcnCss() {
|
|
106
|
+
const { core } = tokens;
|
|
107
|
+
let out = HEADER(
|
|
108
|
+
'Console vocabulary. Values are BARE `H S% L%` triples with no hsl() wrapper,\n' +
|
|
109
|
+
' * because shadcn composes them as `hsl(var(--primary) / <alpha>)` — wrapping\n' +
|
|
110
|
+
' * them here would silently break every alpha variant.\n' +
|
|
111
|
+
' *\n' +
|
|
112
|
+
' * Scope: every token whose value is DETERMINED BY the brand blue — the blue\n' +
|
|
113
|
+
' * itself wherever shadcn names it, plus the foreground that has to read on\n' +
|
|
114
|
+
' * top of it. A consumer keeps ownership of everything else in its scale\n' +
|
|
115
|
+
' * (background, card, popover, chart-*, and the non-primary sidebar-*).'
|
|
116
|
+
);
|
|
117
|
+
const block = (sel, hex) => {
|
|
118
|
+
const t = hexToShadcnTriple(hex);
|
|
119
|
+
return (
|
|
120
|
+
`${sel} {\n` +
|
|
121
|
+
` --primary: ${t};\n` +
|
|
122
|
+
' --primary-foreground: 0 0% 100%;\n' +
|
|
123
|
+
` --ring: ${t};\n` +
|
|
124
|
+
` --sidebar-primary: ${t};\n` +
|
|
125
|
+
' --sidebar-primary-foreground: 0 0% 100%;\n' +
|
|
126
|
+
` --sidebar-ring: ${t};\n` +
|
|
127
|
+
'}\n'
|
|
128
|
+
);
|
|
129
|
+
};
|
|
130
|
+
out += block(':root', core.primary.value);
|
|
131
|
+
out += '\n';
|
|
132
|
+
out += block('.dark', core.primaryBright.value);
|
|
133
|
+
return out;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/* ── Docs (Docusaurus / Infima) vocabulary ─────────────────────────────── */
|
|
137
|
+
|
|
138
|
+
function infimaCss() {
|
|
139
|
+
const { core } = tokens;
|
|
140
|
+
// Infima wants a six-step ramp around primary, and it is built by scaling
|
|
141
|
+
// LIGHTNESS in HSL — hue and saturation are held constant. The factors below
|
|
142
|
+
// are Docusaurus's own conventional steps.
|
|
143
|
+
//
|
|
144
|
+
// Do not "simplify" this to an RGB mix toward white/black. That adds white to
|
|
145
|
+
// all three channels equally and therefore desaturates: checked against the
|
|
146
|
+
// docs site's existing ramp, a mix-to-white `lightest` landed at 51.5%
|
|
147
|
+
// saturation against the correct 71.1% — a visibly greyer, washed-out blue.
|
|
148
|
+
const ramp = (base) => ({
|
|
149
|
+
darkest: scaleLightness(base, 0.7),
|
|
150
|
+
darker: scaleLightness(base, 0.85),
|
|
151
|
+
dark: scaleLightness(base, 0.9),
|
|
152
|
+
base,
|
|
153
|
+
light: scaleLightness(base, 1.1),
|
|
154
|
+
lighter: scaleLightness(base, 1.15),
|
|
155
|
+
lightest: scaleLightness(base, 1.3),
|
|
156
|
+
});
|
|
157
|
+
const emit = (sel, base) => {
|
|
158
|
+
const r = ramp(base);
|
|
159
|
+
let s = `${sel} {\n`;
|
|
160
|
+
s += ` --ifm-color-primary: ${r.base};\n`;
|
|
161
|
+
s += ` --ifm-color-primary-dark: ${r.dark};\n`;
|
|
162
|
+
s += ` --ifm-color-primary-darker: ${r.darker};\n`;
|
|
163
|
+
s += ` --ifm-color-primary-darkest: ${r.darkest};\n`;
|
|
164
|
+
s += ` --ifm-color-primary-light: ${r.light};\n`;
|
|
165
|
+
s += ` --ifm-color-primary-lighter: ${r.lighter};\n`;
|
|
166
|
+
s += ` --ifm-color-primary-lightest: ${r.lightest};\n`;
|
|
167
|
+
s += '}\n';
|
|
168
|
+
return s;
|
|
169
|
+
};
|
|
170
|
+
let out = HEADER(
|
|
171
|
+
'Docs vocabulary (Docusaurus/Infima). Infima needs a five-step ramp either\n' +
|
|
172
|
+
' * side of primary; it is generated from the single canonical value so the\n' +
|
|
173
|
+
' * steps cannot drift out of order.\n' +
|
|
174
|
+
' *\n' +
|
|
175
|
+
' * Brand colours only — the docs site owns its own background/surface theming.'
|
|
176
|
+
);
|
|
177
|
+
out += emit(':root', tokens.core.primary.value);
|
|
178
|
+
out += '\n';
|
|
179
|
+
out += emit("[data-theme='dark']", core.primaryBright.value);
|
|
180
|
+
return out;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/* ── JSON, for anything that would rather read data than parse CSS ─────── */
|
|
184
|
+
|
|
185
|
+
function flatJson() {
|
|
186
|
+
const flat = {};
|
|
187
|
+
for (const [group, entries] of Object.entries(tokens)) {
|
|
188
|
+
if (group.startsWith('$')) continue;
|
|
189
|
+
for (const [key, def] of Object.entries(entries)) {
|
|
190
|
+
if (key.startsWith('$')) continue;
|
|
191
|
+
flat[`${group}.${key}`] = def.value;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
const derived = {};
|
|
195
|
+
for (const k of ['primary', 'primaryBright', 'primaryDeep', 'primaryPale']) {
|
|
196
|
+
derived[k] = {
|
|
197
|
+
hex: tokens.core[k].value,
|
|
198
|
+
hsl: hexToHsl(tokens.core[k].value),
|
|
199
|
+
shadcn: hexToShadcnTriple(tokens.core[k].value),
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
return JSON.stringify({ flat, derived }, null, 2) + '\n';
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/* ── Drive ─────────────────────────────────────────────────────────────── */
|
|
206
|
+
|
|
207
|
+
const OUTPUTS = [
|
|
208
|
+
['css/brand.css', brandCss()],
|
|
209
|
+
['css/shadcn.css', shadcnCss()],
|
|
210
|
+
['css/infima.css', infimaCss()],
|
|
211
|
+
['tokens.generated.json', flatJson()],
|
|
212
|
+
];
|
|
213
|
+
|
|
214
|
+
const check = process.argv.includes('--check');
|
|
215
|
+
let stale = [];
|
|
216
|
+
|
|
217
|
+
mkdirSync(join(ROOT, 'css'), { recursive: true });
|
|
218
|
+
for (const [rel, content] of OUTPUTS) {
|
|
219
|
+
const path = join(ROOT, rel);
|
|
220
|
+
if (check) {
|
|
221
|
+
let existing = null;
|
|
222
|
+
try {
|
|
223
|
+
existing = readFileSync(path, 'utf8');
|
|
224
|
+
} catch {
|
|
225
|
+
/* missing counts as stale */
|
|
226
|
+
}
|
|
227
|
+
if (existing !== content) stale.push(rel);
|
|
228
|
+
} else {
|
|
229
|
+
writeFileSync(path, content, 'utf8');
|
|
230
|
+
console.log(`wrote ${rel}`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (check) {
|
|
235
|
+
if (stale.length) {
|
|
236
|
+
console.error(
|
|
237
|
+
`error: generated files are stale or missing:\n ${stale.join('\n ')}\n` +
|
|
238
|
+
`Run \`npm run generate -w @devicechain/brand\` and commit the result.`
|
|
239
|
+
);
|
|
240
|
+
process.exit(1);
|
|
241
|
+
}
|
|
242
|
+
console.log('generated files are up to date');
|
|
243
|
+
}
|
package/tools/verify.mjs
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
// Copyright The DeviceChain Authors
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
// Report where a consumer's declared brand values disagree with tokens.json.
|
|
5
|
+
//
|
|
6
|
+
// node tools/verify.mjs
|
|
7
|
+
// DC_WEBSITE=/path/to/devicechain-website node tools/verify.mjs
|
|
8
|
+
//
|
|
9
|
+
// This exists because "keep it in step with the console" was a COMMENT, not a
|
|
10
|
+
// guarantee, and it had already failed: the marketing site's lifted blue had
|
|
11
|
+
// drifted to a third value that neither the console nor the docs used, and the
|
|
12
|
+
// console's rounded HSL triple was not the same colour as the hex everyone else
|
|
13
|
+
// wrote. A comment cannot catch that. A script run in CI can.
|
|
14
|
+
//
|
|
15
|
+
// Deliberately read-only and advisory about WHICH way to fix things: it prints
|
|
16
|
+
// what disagrees and exits non-zero, but never rewrites a consumer, because
|
|
17
|
+
// changing a shipped colour is a visual decision, not a mechanical one.
|
|
18
|
+
|
|
19
|
+
import { readFileSync } from 'node:fs';
|
|
20
|
+
import { dirname, join } from 'node:path';
|
|
21
|
+
import { fileURLToPath } from 'node:url';
|
|
22
|
+
import { hexToHsl, hexToShadcnTriple } from './color.mjs';
|
|
23
|
+
|
|
24
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
25
|
+
const ROOT = join(HERE, '..');
|
|
26
|
+
const REPO = join(ROOT, '..', '..', '..');
|
|
27
|
+
const tokens = JSON.parse(readFileSync(join(ROOT, 'tokens.json'), 'utf8'));
|
|
28
|
+
|
|
29
|
+
const PRIMARY = tokens.core.primary.value;
|
|
30
|
+
const BRIGHT = tokens.core.primaryBright.value;
|
|
31
|
+
|
|
32
|
+
const findings = [];
|
|
33
|
+
const checked = [];
|
|
34
|
+
|
|
35
|
+
function read(rel) {
|
|
36
|
+
try {
|
|
37
|
+
return readFileSync(join(REPO, rel), 'utf8');
|
|
38
|
+
} catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Last declaration wins, matching the cascade, so scan all and keep the last. */
|
|
44
|
+
function declared(css, prop, withinSelector) {
|
|
45
|
+
const scope = withinSelector
|
|
46
|
+
? (css.match(
|
|
47
|
+
new RegExp(
|
|
48
|
+
`${withinSelector.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*\\{([^}]*)\\}`,
|
|
49
|
+
'g'
|
|
50
|
+
)
|
|
51
|
+
) || []).join('\n')
|
|
52
|
+
: css;
|
|
53
|
+
const re = new RegExp(`--${prop}\\s*:\\s*([^;]+);`, 'g');
|
|
54
|
+
let m;
|
|
55
|
+
let last = null;
|
|
56
|
+
while ((m = re.exec(scope))) last = m[1].trim();
|
|
57
|
+
return last;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function compare(where, label, actual, expected, note) {
|
|
61
|
+
checked.push(where + ' · ' + label);
|
|
62
|
+
if (actual === null) {
|
|
63
|
+
findings.push({ where, label, actual: '(not declared)', expected, note });
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (actual.toLowerCase() !== expected.toLowerCase()) {
|
|
67
|
+
findings.push({ where, label, actual, expected, note });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/* ── shadcn-triple consumers ───────────────────────────────────────────── */
|
|
72
|
+
//
|
|
73
|
+
// BOTH front-end apps, not just the console. Checking only the console was a
|
|
74
|
+
// real hole: apps/dashboard/src/theme.css carries the same token contract (it
|
|
75
|
+
// is what @devicechain/widgets reads, so a dashboard renders identically in
|
|
76
|
+
// either host) and had drifted to the same wrong triple — unnoticed, because
|
|
77
|
+
// nothing looked at it. A gate that covers one of two consumers mostly teaches
|
|
78
|
+
// you to trust it.
|
|
79
|
+
for (const rel of [
|
|
80
|
+
'frontend/apps/console/src/index.css',
|
|
81
|
+
'frontend/apps/dashboard/src/theme.css',
|
|
82
|
+
]) {
|
|
83
|
+
const css = read(rel);
|
|
84
|
+
if (!css) {
|
|
85
|
+
console.warn(`skip: ${rel} not found`);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (/@import\s+['"]@devicechain\/brand\/css\/shadcn\.css['"]/.test(css)) {
|
|
89
|
+
// A MIGRATED consumer declares nothing — it imports. See the docs branch
|
|
90
|
+
// below for why an un-special-cased scan here is worse than useless.
|
|
91
|
+
console.log(`ok: ${rel} imports the generated triples — nothing to compare`);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
// The :root block is light mode; .dark carries the lifted values.
|
|
95
|
+
compare(
|
|
96
|
+
rel,
|
|
97
|
+
'light --primary',
|
|
98
|
+
declared(css, 'primary', ':root'),
|
|
99
|
+
hexToShadcnTriple(PRIMARY),
|
|
100
|
+
`rounded triples do not round-trip: '197 71% 42%' renders #1f8cb7, not ${PRIMARY}`
|
|
101
|
+
);
|
|
102
|
+
compare(rel, 'dark --primary', declared(css, 'primary', '.dark'), hexToShadcnTriple(BRIGHT));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/* ── Docs: Infima hex, base AND the full ramp ──────────────────────────── */
|
|
106
|
+
{
|
|
107
|
+
const rel = 'docs/src/css/custom.css';
|
|
108
|
+
const css = read(rel);
|
|
109
|
+
if (!css) {
|
|
110
|
+
console.warn(`skip: ${rel} not found`);
|
|
111
|
+
} else if (/@import\s+['"]@devicechain\/brand\/css\/infima\.css['"]/.test(css)) {
|
|
112
|
+
// A MIGRATED consumer declares nothing — it imports. Without this branch the
|
|
113
|
+
// declaration scan below finds nothing and reports 14 "(not declared)"
|
|
114
|
+
// failures against a consumer that is in fact perfectly correct, which is
|
|
115
|
+
// the fastest way to teach everyone to ignore this script.
|
|
116
|
+
console.log(`ok: ${rel} imports the generated ramp — nothing to compare`);
|
|
117
|
+
} else {
|
|
118
|
+
// Checking only the base colour is NOT enough, and assuming otherwise gave
|
|
119
|
+
// a false all-clear once already: the docs' base matched exactly while its
|
|
120
|
+
// hand-built DARK ramp differed by up to 45/255 on --ifm-color-primary-
|
|
121
|
+
// lightest. Infima renders links, hovers and admonitions out of the ramp,
|
|
122
|
+
// not the base, so the ramp is the part a reader actually sees.
|
|
123
|
+
const generated = readFileSync(join(ROOT, 'css/infima.css'), 'utf8');
|
|
124
|
+
const STEPS = ['', '-dark', '-darker', '-darkest', '-light', '-lighter', '-lightest'];
|
|
125
|
+
for (const [theme, selector] of [
|
|
126
|
+
['light', ':root'],
|
|
127
|
+
['dark', "[data-theme='dark']"],
|
|
128
|
+
]) {
|
|
129
|
+
for (const step of STEPS) {
|
|
130
|
+
const prop = `ifm-color-primary${step}`;
|
|
131
|
+
compare(
|
|
132
|
+
rel,
|
|
133
|
+
`${theme} --${prop}`,
|
|
134
|
+
declared(css, prop, selector),
|
|
135
|
+
declared(generated, prop, selector)
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/* ── Marketing site: a SEPARATE REPO, so opt in by path ────────────────── */
|
|
143
|
+
{
|
|
144
|
+
const base = process.env.DC_WEBSITE;
|
|
145
|
+
if (!base) {
|
|
146
|
+
console.warn(
|
|
147
|
+
'skip: marketing site not checked — set DC_WEBSITE=/path/to/devicechain-website'
|
|
148
|
+
);
|
|
149
|
+
} else {
|
|
150
|
+
let css = null;
|
|
151
|
+
try {
|
|
152
|
+
css = readFileSync(join(base, 'brand.css'), 'utf8');
|
|
153
|
+
} catch {
|
|
154
|
+
console.warn(`skip: ${join(base, 'brand.css')} not found`);
|
|
155
|
+
}
|
|
156
|
+
if (css) {
|
|
157
|
+
const rel = `${base}/brand.css`;
|
|
158
|
+
compare(rel, '--brand', declared(css, 'brand', ':root'), PRIMARY);
|
|
159
|
+
compare(
|
|
160
|
+
rel,
|
|
161
|
+
'--brand-bright',
|
|
162
|
+
declared(css, 'brand-bright', ':root'),
|
|
163
|
+
BRIGHT,
|
|
164
|
+
'the site had drifted to a third value that neither console nor docs used'
|
|
165
|
+
);
|
|
166
|
+
compare(rel, '--bg-deep', declared(css, 'bg-deep', ':root'), tokens.surface.bgDeep.value);
|
|
167
|
+
for (const k of ['aqua', 'violet', 'magenta', 'amber']) {
|
|
168
|
+
compare(rel, `--${k}`, declared(css, k, ':root'), tokens.accents[k].value);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/* ── Report ────────────────────────────────────────────────────────────── */
|
|
175
|
+
|
|
176
|
+
const hsl = hexToHsl(PRIMARY);
|
|
177
|
+
console.log(
|
|
178
|
+
`\ncanonical primary ${PRIMARY} = hsl(${hsl.h} ${hsl.s}% ${hsl.l}%) = shadcn "${hexToShadcnTriple(
|
|
179
|
+
PRIMARY
|
|
180
|
+
)}"`
|
|
181
|
+
);
|
|
182
|
+
console.log(`canonical lifted ${BRIGHT} = shadcn "${hexToShadcnTriple(BRIGHT)}"\n`);
|
|
183
|
+
console.log(`checked ${checked.length} declaration(s)`);
|
|
184
|
+
|
|
185
|
+
if (!findings.length) {
|
|
186
|
+
console.log('no drift — every consumer agrees with tokens.json');
|
|
187
|
+
process.exit(0);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
console.log(`\n${findings.length} disagreement(s) with tokens.json:\n`);
|
|
191
|
+
for (const f of findings) {
|
|
192
|
+
console.log(` ${f.where}`);
|
|
193
|
+
console.log(` ${f.label}`);
|
|
194
|
+
console.log(` declared: ${f.actual}`);
|
|
195
|
+
console.log(` canonical: ${f.expected}`);
|
|
196
|
+
if (f.note) console.log(` note: ${f.note}`);
|
|
197
|
+
console.log('');
|
|
198
|
+
}
|
|
199
|
+
console.log(
|
|
200
|
+
'Each of these is a VISUAL decision. Migrate a consumer deliberately —\n' +
|
|
201
|
+
'do not bulk-rewrite shipped colours to silence this script.'
|
|
202
|
+
);
|
|
203
|
+
process.exit(1);
|