cjm-design-system 0.0.1__tar.gz
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.
- cjm_design_system-0.0.1/PKG-INFO +66 -0
- cjm_design_system-0.0.1/README.md +54 -0
- cjm_design_system-0.0.1/cjm_design_system/__init__.py +15 -0
- cjm_design_system-0.0.1/cjm_design_system/systems/__init__.py +42 -0
- cjm_design_system-0.0.1/cjm_design_system/systems/classical/fonts/CormorantGaramond-Italic[wght].ttf +0 -0
- cjm_design_system-0.0.1/cjm_design_system/systems/classical/fonts/CormorantGaramond[wght].ttf +0 -0
- cjm_design_system-0.0.1/cjm_design_system/systems/classical/fonts/Lora-Italic[wght].ttf +0 -0
- cjm_design_system-0.0.1/cjm_design_system/systems/classical/fonts/Lora[wght].ttf +0 -0
- cjm_design_system-0.0.1/cjm_design_system/systems/classical/fonts/OFL.txt +93 -0
- cjm_design_system-0.0.1/cjm_design_system/systems/classical/tokens.json +41 -0
- cjm_design_system-0.0.1/cjm_design_system/systems/netrunner/fonts/CourierPrime-Bold.ttf +0 -0
- cjm_design_system-0.0.1/cjm_design_system/systems/netrunner/fonts/CourierPrime-Italic.ttf +0 -0
- cjm_design_system-0.0.1/cjm_design_system/systems/netrunner/fonts/CourierPrime-Regular.ttf +0 -0
- cjm_design_system-0.0.1/cjm_design_system/systems/netrunner/fonts/OFL.txt +93 -0
- cjm_design_system-0.0.1/cjm_design_system/systems/netrunner/fonts/Silkscreen-Bold.ttf +0 -0
- cjm_design_system-0.0.1/cjm_design_system/systems/netrunner/fonts/Silkscreen-Regular.ttf +0 -0
- cjm_design_system-0.0.1/cjm_design_system/systems/netrunner/tokens.json +64 -0
- cjm_design_system-0.0.1/cjm_design_system/tokens.py +333 -0
- cjm_design_system-0.0.1/cjm_design_system/tools/__init__.py +5 -0
- cjm_design_system-0.0.1/cjm_design_system/tools/build.py +37 -0
- cjm_design_system-0.0.1/cjm_design_system/tools/css_to_tokens.py +123 -0
- cjm_design_system-0.0.1/cjm_design_system.egg-info/PKG-INFO +66 -0
- cjm_design_system-0.0.1/cjm_design_system.egg-info/SOURCES.txt +26 -0
- cjm_design_system-0.0.1/cjm_design_system.egg-info/dependency_links.txt +1 -0
- cjm_design_system-0.0.1/cjm_design_system.egg-info/top_level.txt +1 -0
- cjm_design_system-0.0.1/pyproject.toml +27 -0
- cjm_design_system-0.0.1/setup.cfg +4 -0
- cjm_design_system-0.0.1/tests/test_tokens.py +109 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cjm-design-system
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: A design system as data (ruling a439c226): token schema v1 with its check and its headless resolution into one flat vocabulary, the CSS custom-properties projection for the web, the vendored seed systems (Classical, Netrunner) with their fonts, and the importer for a web-first system's CSS. No Qt: the Qt kit (cjm-substrate-qt-kit) consumes the same tokens. Born on-graph.
|
|
5
|
+
Author-email: "Christian J. Mills" <9126128+cj-mills@users.noreply.github.com>
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
Project-URL: Repository, https://github.com/cj-mills/cjm-design-system
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
10
|
+
Requires-Python: >=3.12
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# cjm-design-system
|
|
14
|
+
|
|
15
|
+
<!-- generated from the context graph by `cjm-context-graph readme` — do not edit by hand; edit the graph (the urge to hand-edit = move it on-graph) -->
|
|
16
|
+
|
|
17
|
+
A design system as data (ruling a439c226): token schema v1 with its check and its headless resolution into one flat vocabulary, the CSS custom-properties projection for the web, the vendored seed systems Classical and Netrunner with their OFL fonts, and the importer for a web-first system's CSS. Nothing here imports Qt, so a web build never needs PySide6; the Qt kit (cjm-substrate-qt-kit) consumes the same tokens and keeps only what Qt reads. Born out of the kit by design 0858bbd0, the theme half of the public-site redesign build 8079ae0f.
|
|
18
|
+
|
|
19
|
+
## Modules
|
|
20
|
+
|
|
21
|
+
- **`cjm_design_system.__init__`** — A design system as DATA (ruling a439c226): one canonical tokens file per
|
|
22
|
+
- **`cjm_design_system.systems.__init__`** — The design systems this library ships as DATA (ruling a439c226): one
|
|
23
|
+
- **`cjm_design_system.tokens`** — Token schema v1 — a design system as DATA, resolved headlessly (ruling
|
|
24
|
+
- **`cjm_design_system.tools.__init__`** — Design-system tools (run as modules):
|
|
25
|
+
- **`cjm_design_system.tools.build`** — Build the static web projections of a design system: the CSS
|
|
26
|
+
- **`cjm_design_system.tools.css_to_tokens`** — Import a web-first design system's CSS into a schema-v1 tokens file.
|
|
27
|
+
|
|
28
|
+
## API
|
|
29
|
+
|
|
30
|
+
### `cjm_design_system.systems.__init__`
|
|
31
|
+
|
|
32
|
+
- `available` _function_ — Every vendored system slug (a directory carrying a tokens.json).
|
|
33
|
+
- `locate` _function_ — The directory of a system: a vendored slug, a directory holding a
|
|
34
|
+
- `tokens_path` _function_
|
|
35
|
+
|
|
36
|
+
### `cjm_design_system.tokens`
|
|
37
|
+
|
|
38
|
+
- `SchemaError` _class_ — A tokens file that violates schema v1 — every problem listed, so one
|
|
39
|
+
- `check` _function_ — Refuse a malformed system loudly: every missing / mistyped field named
|
|
40
|
+
- `load` _function_ — Read a tokens file (JSON). `check` it before resolving anything.
|
|
41
|
+
- `mix` _function_ — Flatten `fg` at opacity `t` over `bg` -> a solid hex (QPalette, painters,
|
|
42
|
+
- `mode_for_scheme` _function_ — The mode the system maps an OS scheme ("light" / "dark") onto — None
|
|
43
|
+
- `modes` _function_
|
|
44
|
+
- `resolve` _function_ — tokens + mode -> the flat vocabulary every projection reads: the QSS
|
|
45
|
+
- `rgba` _function_ — QSS rgba() with a 0-255 alpha (the form every Qt 6 version parses).
|
|
46
|
+
- `slug` _function_ — The system's file-system / preference slug: lower-case, spaces -> dashes.
|
|
47
|
+
- `to_css` _function_ — The CSS custom-properties layer for a system — the ruling's second
|
|
48
|
+
- `to_hex` _function_
|
|
49
|
+
|
|
50
|
+
### `cjm_design_system.tools.build`
|
|
51
|
+
|
|
52
|
+
- `main` _function_
|
|
53
|
+
|
|
54
|
+
### `cjm_design_system.tools.css_to_tokens`
|
|
55
|
+
|
|
56
|
+
- `convert` _function_
|
|
57
|
+
- `darker` _function_
|
|
58
|
+
- `family` _function_
|
|
59
|
+
- `main` _function_
|
|
60
|
+
- `parse_root` _function_
|
|
61
|
+
- `px` _function_
|
|
62
|
+
- `ramp` _function_
|
|
63
|
+
|
|
64
|
+
## Dependencies
|
|
65
|
+
|
|
66
|
+
**Used by:** `cjm-context-graph-projection`, `cjm-substrate-qt-kit`
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# cjm-design-system
|
|
2
|
+
|
|
3
|
+
<!-- generated from the context graph by `cjm-context-graph readme` — do not edit by hand; edit the graph (the urge to hand-edit = move it on-graph) -->
|
|
4
|
+
|
|
5
|
+
A design system as data (ruling a439c226): token schema v1 with its check and its headless resolution into one flat vocabulary, the CSS custom-properties projection for the web, the vendored seed systems Classical and Netrunner with their OFL fonts, and the importer for a web-first system's CSS. Nothing here imports Qt, so a web build never needs PySide6; the Qt kit (cjm-substrate-qt-kit) consumes the same tokens and keeps only what Qt reads. Born out of the kit by design 0858bbd0, the theme half of the public-site redesign build 8079ae0f.
|
|
6
|
+
|
|
7
|
+
## Modules
|
|
8
|
+
|
|
9
|
+
- **`cjm_design_system.__init__`** — A design system as DATA (ruling a439c226): one canonical tokens file per
|
|
10
|
+
- **`cjm_design_system.systems.__init__`** — The design systems this library ships as DATA (ruling a439c226): one
|
|
11
|
+
- **`cjm_design_system.tokens`** — Token schema v1 — a design system as DATA, resolved headlessly (ruling
|
|
12
|
+
- **`cjm_design_system.tools.__init__`** — Design-system tools (run as modules):
|
|
13
|
+
- **`cjm_design_system.tools.build`** — Build the static web projections of a design system: the CSS
|
|
14
|
+
- **`cjm_design_system.tools.css_to_tokens`** — Import a web-first design system's CSS into a schema-v1 tokens file.
|
|
15
|
+
|
|
16
|
+
## API
|
|
17
|
+
|
|
18
|
+
### `cjm_design_system.systems.__init__`
|
|
19
|
+
|
|
20
|
+
- `available` _function_ — Every vendored system slug (a directory carrying a tokens.json).
|
|
21
|
+
- `locate` _function_ — The directory of a system: a vendored slug, a directory holding a
|
|
22
|
+
- `tokens_path` _function_
|
|
23
|
+
|
|
24
|
+
### `cjm_design_system.tokens`
|
|
25
|
+
|
|
26
|
+
- `SchemaError` _class_ — A tokens file that violates schema v1 — every problem listed, so one
|
|
27
|
+
- `check` _function_ — Refuse a malformed system loudly: every missing / mistyped field named
|
|
28
|
+
- `load` _function_ — Read a tokens file (JSON). `check` it before resolving anything.
|
|
29
|
+
- `mix` _function_ — Flatten `fg` at opacity `t` over `bg` -> a solid hex (QPalette, painters,
|
|
30
|
+
- `mode_for_scheme` _function_ — The mode the system maps an OS scheme ("light" / "dark") onto — None
|
|
31
|
+
- `modes` _function_
|
|
32
|
+
- `resolve` _function_ — tokens + mode -> the flat vocabulary every projection reads: the QSS
|
|
33
|
+
- `rgba` _function_ — QSS rgba() with a 0-255 alpha (the form every Qt 6 version parses).
|
|
34
|
+
- `slug` _function_ — The system's file-system / preference slug: lower-case, spaces -> dashes.
|
|
35
|
+
- `to_css` _function_ — The CSS custom-properties layer for a system — the ruling's second
|
|
36
|
+
- `to_hex` _function_
|
|
37
|
+
|
|
38
|
+
### `cjm_design_system.tools.build`
|
|
39
|
+
|
|
40
|
+
- `main` _function_
|
|
41
|
+
|
|
42
|
+
### `cjm_design_system.tools.css_to_tokens`
|
|
43
|
+
|
|
44
|
+
- `convert` _function_
|
|
45
|
+
- `darker` _function_
|
|
46
|
+
- `family` _function_
|
|
47
|
+
- `main` _function_
|
|
48
|
+
- `parse_root` _function_
|
|
49
|
+
- `px` _function_
|
|
50
|
+
- `ramp` _function_
|
|
51
|
+
|
|
52
|
+
## Dependencies
|
|
53
|
+
|
|
54
|
+
**Used by:** `cjm-context-graph-projection`, `cjm-substrate-qt-kit`
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""A design system as DATA (ruling a439c226): one canonical tokens file per
|
|
2
|
+
system, from which every consumer derives what it renders — the web (the CSS
|
|
3
|
+
custom-properties layer for the Quarto sites, 8079ae0f) and the Qt kit
|
|
4
|
+
(cjm-substrate-qt-kit: QPalette + QSS + fonts + icons) alike.
|
|
5
|
+
|
|
6
|
+
This library holds the half no consumer class owns (design 0858bbd0): token
|
|
7
|
+
schema v1 with its check and its headless resolution into one flat vocabulary
|
|
8
|
+
(`tokens`), the vendored seed systems' data — each system's tokens.json and
|
|
9
|
+
the font files it names (`systems`: Classical and Netrunner) — and the
|
|
10
|
+
importer for a web-first system's CSS (`tools.css_to_tokens`). Nothing here
|
|
11
|
+
imports Qt, so a web build never needs PySide6; what only Qt reads (QSS
|
|
12
|
+
templates, painted widgets, the Theme runtime) stays in the kit.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
__version__ = "0.0.1"
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""The design systems this library ships as DATA (ruling a439c226): one
|
|
2
|
+
directory per system under this package — `tokens.json` (schema v1) and
|
|
3
|
+
`fonts/` (the OFL files its tokens name, with their licence). What only a
|
|
4
|
+
consumer reads lives with that consumer: the Qt kit keeps each system's QSS
|
|
5
|
+
templates and painted widgets under its own `systems/<slug>/` (design
|
|
6
|
+
0858bbd0).
|
|
7
|
+
|
|
8
|
+
available() -> ["classical", "netrunner"]
|
|
9
|
+
locate("netrunner") -> the system's directory
|
|
10
|
+
locate("/path/to/mysystem/tokens.json") -> that file's directory
|
|
11
|
+
|
|
12
|
+
Custom systems come AFTER the gallery (abb6360d); a path-valued system is
|
|
13
|
+
how one is tried before it is vendored."""
|
|
14
|
+
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import List, Union
|
|
17
|
+
|
|
18
|
+
SYSTEMS_DIR = Path(__file__).parent
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def available() -> List[str]:
|
|
22
|
+
"""Every vendored system slug (a directory carrying a tokens.json)."""
|
|
23
|
+
return sorted(p.parent.name for p in SYSTEMS_DIR.glob("*/tokens.json"))
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def locate(system: Union[str, Path]) -> Path:
|
|
27
|
+
"""The directory of a system: a vendored slug, a directory holding a
|
|
28
|
+
tokens.json, or a tokens.json path. Raises KeyError naming the slugs
|
|
29
|
+
available when nothing matches."""
|
|
30
|
+
p = Path(system)
|
|
31
|
+
if p.suffix == ".json" and p.is_file():
|
|
32
|
+
return p.parent
|
|
33
|
+
if p.is_dir() and (p / "tokens.json").is_file():
|
|
34
|
+
return p
|
|
35
|
+
if (SYSTEMS_DIR / str(system) / "tokens.json").is_file():
|
|
36
|
+
return SYSTEMS_DIR / str(system)
|
|
37
|
+
raise KeyError(f"no design system {system!r} — vendored: {available()}; "
|
|
38
|
+
"or pass a directory holding tokens.json")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def tokens_path(system: Union[str, Path]) -> Path:
|
|
42
|
+
return locate(system) / "tokens.json"
|
cjm_design_system-0.0.1/cjm_design_system/systems/classical/fonts/CormorantGaramond-Italic[wght].ttf
ADDED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
Copyright 2011 The Lora Project Authors (https://github.com/cyrealtype/Lora-Cyrillic), with Reserved Font Name "Lora".
|
|
2
|
+
|
|
3
|
+
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
|
4
|
+
This license is copied below, and is also available with a FAQ at:
|
|
5
|
+
https://scripts.sil.org/OFL
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
-----------------------------------------------------------
|
|
9
|
+
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
|
10
|
+
-----------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
PREAMBLE
|
|
13
|
+
The goals of the Open Font License (OFL) are to stimulate worldwide
|
|
14
|
+
development of collaborative font projects, to support the font creation
|
|
15
|
+
efforts of academic and linguistic communities, and to provide a free and
|
|
16
|
+
open framework in which fonts may be shared and improved in partnership
|
|
17
|
+
with others.
|
|
18
|
+
|
|
19
|
+
The OFL allows the licensed fonts to be used, studied, modified and
|
|
20
|
+
redistributed freely as long as they are not sold by themselves. The
|
|
21
|
+
fonts, including any derivative works, can be bundled, embedded,
|
|
22
|
+
redistributed and/or sold with any software provided that any reserved
|
|
23
|
+
names are not used by derivative works. The fonts and derivatives,
|
|
24
|
+
however, cannot be released under any other type of license. The
|
|
25
|
+
requirement for fonts to remain under this license does not apply
|
|
26
|
+
to any document created using the fonts or their derivatives.
|
|
27
|
+
|
|
28
|
+
DEFINITIONS
|
|
29
|
+
"Font Software" refers to the set of files released by the Copyright
|
|
30
|
+
Holder(s) under this license and clearly marked as such. This may
|
|
31
|
+
include source files, build scripts and documentation.
|
|
32
|
+
|
|
33
|
+
"Reserved Font Name" refers to any names specified as such after the
|
|
34
|
+
copyright statement(s).
|
|
35
|
+
|
|
36
|
+
"Original Version" refers to the collection of Font Software components as
|
|
37
|
+
distributed by the Copyright Holder(s).
|
|
38
|
+
|
|
39
|
+
"Modified Version" refers to any derivative made by adding to, deleting,
|
|
40
|
+
or substituting -- in part or in whole -- any of the components of the
|
|
41
|
+
Original Version, by changing formats or by porting the Font Software to a
|
|
42
|
+
new environment.
|
|
43
|
+
|
|
44
|
+
"Author" refers to any designer, engineer, programmer, technical
|
|
45
|
+
writer or other person who contributed to the Font Software.
|
|
46
|
+
|
|
47
|
+
PERMISSION & CONDITIONS
|
|
48
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
49
|
+
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
|
50
|
+
redistribute, and sell modified and unmodified copies of the Font
|
|
51
|
+
Software, subject to the following conditions:
|
|
52
|
+
|
|
53
|
+
1) Neither the Font Software nor any of its individual components,
|
|
54
|
+
in Original or Modified Versions, may be sold by itself.
|
|
55
|
+
|
|
56
|
+
2) Original or Modified Versions of the Font Software may be bundled,
|
|
57
|
+
redistributed and/or sold with any software, provided that each copy
|
|
58
|
+
contains the above copyright notice and this license. These can be
|
|
59
|
+
included either as stand-alone text files, human-readable headers or
|
|
60
|
+
in the appropriate machine-readable metadata fields within text or
|
|
61
|
+
binary files as long as those fields can be easily viewed by the user.
|
|
62
|
+
|
|
63
|
+
3) No Modified Version of the Font Software may use the Reserved Font
|
|
64
|
+
Name(s) unless explicit written permission is granted by the corresponding
|
|
65
|
+
Copyright Holder. This restriction only applies to the primary font name as
|
|
66
|
+
presented to the users.
|
|
67
|
+
|
|
68
|
+
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
|
69
|
+
Software shall not be used to promote, endorse or advertise any
|
|
70
|
+
Modified Version, except to acknowledge the contribution(s) of the
|
|
71
|
+
Copyright Holder(s) and the Author(s) or with their explicit written
|
|
72
|
+
permission.
|
|
73
|
+
|
|
74
|
+
5) The Font Software, modified or unmodified, in part or in whole,
|
|
75
|
+
must be distributed entirely under this license, and must not be
|
|
76
|
+
distributed under any other license. The requirement for fonts to
|
|
77
|
+
remain under this license does not apply to any document created
|
|
78
|
+
using the Font Software.
|
|
79
|
+
|
|
80
|
+
TERMINATION
|
|
81
|
+
This license becomes null and void if any of the above conditions are
|
|
82
|
+
not met.
|
|
83
|
+
|
|
84
|
+
DISCLAIMER
|
|
85
|
+
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
86
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
|
87
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
|
88
|
+
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
|
89
|
+
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
90
|
+
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
|
91
|
+
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
92
|
+
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
|
93
|
+
OTHER DEALINGS IN THE FONT SOFTWARE.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema": 1,
|
|
3
|
+
"name": "Classical",
|
|
4
|
+
"fonts": {
|
|
5
|
+
"heading": {"family": "Cormorant Garamond", "weight": 600},
|
|
6
|
+
"body": {"family": "Lora", "size": 15},
|
|
7
|
+
"mono": {"family": "", "size": 13},
|
|
8
|
+
"ui": {"family": "Lora", "size": 14},
|
|
9
|
+
"files": ["CormorantGaramond*.ttf", "Lora*.ttf"]
|
|
10
|
+
},
|
|
11
|
+
"space": {"1": 4.6, "2": 9.2, "3": 13.8, "4": 18.4, "6": 27.6, "8": 36.8},
|
|
12
|
+
"radius": {"sm": 2, "md": 4, "lg": 7},
|
|
13
|
+
"type": {"h1": [42, 400], "h2": [32, 400], "h3": [25, 600], "h4": [20, 600], "h5": [16, 600], "title": [17, 600], "kicker": [10, 400], "caption": [11, 400]},
|
|
14
|
+
"ramps": {
|
|
15
|
+
"neutral": ["#f8f4f4", "#eae7e7", "#d7d3d3", "#bab6b6", "#9b9797", "#7d7979", "#605d5d", "#444141", "#2d2b2b"],
|
|
16
|
+
"accent": ["#fff3e4", "#ffe3bf", "#facb8d", "#e1ad66", "#c28d41", "#a06f24", "#7d5411", "#5a3b0a", "#3a270d"]
|
|
17
|
+
},
|
|
18
|
+
"scheme": {"light": "light", "dark": "dark"},
|
|
19
|
+
"reading": {"measure": 68, "line_height": 1.45},
|
|
20
|
+
"icons": {"set": "lucide", "stroke_width": 1.5},
|
|
21
|
+
"modes": {
|
|
22
|
+
"light": {
|
|
23
|
+
"bg": "#f3f2f2", "surface": "#eae9e9", "text": "#201f1d",
|
|
24
|
+
"accent": "#b68235", "accent_pressed": "@accent.600", "accent_text": "@accent.700",
|
|
25
|
+
"tag_bg": "@accent.100", "tag_fg": "@accent.800",
|
|
26
|
+
"tag_neutral_bg": "@neutral.100", "tag_neutral_fg": "@neutral.800",
|
|
27
|
+
"shadow": "@neutral.900", "shadow_strength": 1.0,
|
|
28
|
+
"danger": "#a4382a", "warn": "#8c5e14", "ok": "#3a7a3a",
|
|
29
|
+
"info": "#4a5f96", "meta": "#2a6f7c", "note": "#7a4a8a"
|
|
30
|
+
},
|
|
31
|
+
"dark": {
|
|
32
|
+
"bg": "#1d1b19", "surface": "#272523", "text": "#ede9e3",
|
|
33
|
+
"accent": "#c9964d", "accent_pressed": "@accent.400", "accent_text": "@accent.300",
|
|
34
|
+
"tag_bg": "@accent.900", "tag_fg": "@accent.200",
|
|
35
|
+
"tag_neutral_bg": "@neutral.900", "tag_neutral_fg": "@neutral.200",
|
|
36
|
+
"shadow": "#000000", "shadow_strength": 2.2,
|
|
37
|
+
"danger": "#dc8070", "warn": "#d0a552", "ok": "#7dbb7d",
|
|
38
|
+
"info": "#8fa6d6", "meta": "#67b2bf", "note": "#bb93c9"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
Copyright 2011 The Lora Project Authors (https://github.com/cyrealtype/Lora-Cyrillic), with Reserved Font Name "Lora".
|
|
2
|
+
|
|
3
|
+
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
|
4
|
+
This license is copied below, and is also available with a FAQ at:
|
|
5
|
+
https://scripts.sil.org/OFL
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
-----------------------------------------------------------
|
|
9
|
+
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
|
10
|
+
-----------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
PREAMBLE
|
|
13
|
+
The goals of the Open Font License (OFL) are to stimulate worldwide
|
|
14
|
+
development of collaborative font projects, to support the font creation
|
|
15
|
+
efforts of academic and linguistic communities, and to provide a free and
|
|
16
|
+
open framework in which fonts may be shared and improved in partnership
|
|
17
|
+
with others.
|
|
18
|
+
|
|
19
|
+
The OFL allows the licensed fonts to be used, studied, modified and
|
|
20
|
+
redistributed freely as long as they are not sold by themselves. The
|
|
21
|
+
fonts, including any derivative works, can be bundled, embedded,
|
|
22
|
+
redistributed and/or sold with any software provided that any reserved
|
|
23
|
+
names are not used by derivative works. The fonts and derivatives,
|
|
24
|
+
however, cannot be released under any other type of license. The
|
|
25
|
+
requirement for fonts to remain under this license does not apply
|
|
26
|
+
to any document created using the fonts or their derivatives.
|
|
27
|
+
|
|
28
|
+
DEFINITIONS
|
|
29
|
+
"Font Software" refers to the set of files released by the Copyright
|
|
30
|
+
Holder(s) under this license and clearly marked as such. This may
|
|
31
|
+
include source files, build scripts and documentation.
|
|
32
|
+
|
|
33
|
+
"Reserved Font Name" refers to any names specified as such after the
|
|
34
|
+
copyright statement(s).
|
|
35
|
+
|
|
36
|
+
"Original Version" refers to the collection of Font Software components as
|
|
37
|
+
distributed by the Copyright Holder(s).
|
|
38
|
+
|
|
39
|
+
"Modified Version" refers to any derivative made by adding to, deleting,
|
|
40
|
+
or substituting -- in part or in whole -- any of the components of the
|
|
41
|
+
Original Version, by changing formats or by porting the Font Software to a
|
|
42
|
+
new environment.
|
|
43
|
+
|
|
44
|
+
"Author" refers to any designer, engineer, programmer, technical
|
|
45
|
+
writer or other person who contributed to the Font Software.
|
|
46
|
+
|
|
47
|
+
PERMISSION & CONDITIONS
|
|
48
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
49
|
+
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
|
50
|
+
redistribute, and sell modified and unmodified copies of the Font
|
|
51
|
+
Software, subject to the following conditions:
|
|
52
|
+
|
|
53
|
+
1) Neither the Font Software nor any of its individual components,
|
|
54
|
+
in Original or Modified Versions, may be sold by itself.
|
|
55
|
+
|
|
56
|
+
2) Original or Modified Versions of the Font Software may be bundled,
|
|
57
|
+
redistributed and/or sold with any software, provided that each copy
|
|
58
|
+
contains the above copyright notice and this license. These can be
|
|
59
|
+
included either as stand-alone text files, human-readable headers or
|
|
60
|
+
in the appropriate machine-readable metadata fields within text or
|
|
61
|
+
binary files as long as those fields can be easily viewed by the user.
|
|
62
|
+
|
|
63
|
+
3) No Modified Version of the Font Software may use the Reserved Font
|
|
64
|
+
Name(s) unless explicit written permission is granted by the corresponding
|
|
65
|
+
Copyright Holder. This restriction only applies to the primary font name as
|
|
66
|
+
presented to the users.
|
|
67
|
+
|
|
68
|
+
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
|
69
|
+
Software shall not be used to promote, endorse or advertise any
|
|
70
|
+
Modified Version, except to acknowledge the contribution(s) of the
|
|
71
|
+
Copyright Holder(s) and the Author(s) or with their explicit written
|
|
72
|
+
permission.
|
|
73
|
+
|
|
74
|
+
5) The Font Software, modified or unmodified, in part or in whole,
|
|
75
|
+
must be distributed entirely under this license, and must not be
|
|
76
|
+
distributed under any other license. The requirement for fonts to
|
|
77
|
+
remain under this license does not apply to any document created
|
|
78
|
+
using the Font Software.
|
|
79
|
+
|
|
80
|
+
TERMINATION
|
|
81
|
+
This license becomes null and void if any of the above conditions are
|
|
82
|
+
not met.
|
|
83
|
+
|
|
84
|
+
DISCLAIMER
|
|
85
|
+
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
86
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
|
87
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
|
88
|
+
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
|
89
|
+
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
90
|
+
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
|
91
|
+
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
92
|
+
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
|
93
|
+
OTHER DEALINGS IN THE FONT SOFTWARE.
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema": 1,
|
|
3
|
+
"name": "Netrunner",
|
|
4
|
+
"fonts": {
|
|
5
|
+
"heading": {"family": "Silkscreen", "weight": 700},
|
|
6
|
+
"body": {"family": "Courier Prime", "size": 13},
|
|
7
|
+
"mono": {"family": "Courier Prime", "size": 13},
|
|
8
|
+
"ui": {"family": "Courier Prime", "size": 13},
|
|
9
|
+
"files": ["Silkscreen*.ttf", "CourierPrime*.ttf"]
|
|
10
|
+
},
|
|
11
|
+
"space": {"1": 4, "2": 8, "3": 12, "4": 16, "6": 24, "8": 32},
|
|
12
|
+
"radius": {"sm": 0, "md": 0, "lg": 0},
|
|
13
|
+
"type": {"display": [30, 700], "section": [20, 400], "h1": [26, 700], "h2": [20, 700], "h3": [16, 400], "h4": [14, 400], "h5": [12, 400], "title": [13, 400], "kicker": [11, 400], "caption": [11, 400]},
|
|
14
|
+
"chamfer": 28,
|
|
15
|
+
"chrome": {"titlebar_ink": "fill_text"},
|
|
16
|
+
"ramps": {
|
|
17
|
+
"neutral": ["#e6e1e3", "#b9b1b4", "#8a8185", "#5e5559", "#3d3438", "#271f23", "#1a1216", "#120a0e", "#0a0407"],
|
|
18
|
+
"accent": ["#ffd6e1", "#ff9ab5", "#ff5a87", "#f02a5e", "#e0124c", "#b8003c", "#a00036", "#6a0024", "#3a0014"]
|
|
19
|
+
},
|
|
20
|
+
"reading": {"measure": 80, "line_height": 1.4},
|
|
21
|
+
"icons": {"set": "lucide", "stroke_width": 2.0},
|
|
22
|
+
"modes": {
|
|
23
|
+
"red": {
|
|
24
|
+
"bg": "#0a0407", "surface": "#140a0e", "text": "#e8124f", "accent": "#e8124f",
|
|
25
|
+
"fill": "#a00036", "fill_text": "#2a000e", "secondary": "#2ee6e6", "art": "#4a1018",
|
|
26
|
+
"accent_pressed": "#ff3b6f", "accent_text": "#2ee6e6",
|
|
27
|
+
"tag_bg": "#a00036", "tag_fg": "#2a000e", "tag_neutral_bg": "#27151b", "tag_neutral_fg": "#e8124f",
|
|
28
|
+
"shadow": "#000000", "shadow_strength": 2.0,
|
|
29
|
+
"danger": "#ff3b6f", "warn": "#f2d20c", "ok": "#36e07a", "info": "#2e9bff", "meta": "#2ee6e6", "note": "#ff7ae0"
|
|
30
|
+
},
|
|
31
|
+
"yellow": {
|
|
32
|
+
"bg": "#0a0904", "surface": "#13110a", "text": "#f2d20c", "accent": "#f2d20c",
|
|
33
|
+
"fill": "#9c8600", "fill_text": "#1c1700", "secondary": "#ff3b6f", "art": "#3f3608",
|
|
34
|
+
"accent_pressed": "#fff066", "accent_text": "#ff3b6f",
|
|
35
|
+
"tag_bg": "#9c8600", "tag_fg": "#1c1700", "tag_neutral_bg": "#26220f", "tag_neutral_fg": "#f2d20c",
|
|
36
|
+
"shadow": "#000000", "shadow_strength": 2.0,
|
|
37
|
+
"danger": "#ff3b6f", "warn": "#f2d20c", "ok": "#36e07a", "info": "#2e9bff", "meta": "#2ee6e6", "note": "#ff7ae0"
|
|
38
|
+
},
|
|
39
|
+
"green": {
|
|
40
|
+
"bg": "#040a06", "surface": "#09130c", "text": "#36e07a", "accent": "#36e07a",
|
|
41
|
+
"fill": "#0f7f40", "fill_text": "#00160a", "secondary": "#e6e62e", "art": "#0e3a1f",
|
|
42
|
+
"accent_pressed": "#7dffb0", "accent_text": "#e6e62e",
|
|
43
|
+
"tag_bg": "#0f7f40", "tag_fg": "#00160a", "tag_neutral_bg": "#10261a", "tag_neutral_fg": "#36e07a",
|
|
44
|
+
"shadow": "#000000", "shadow_strength": 2.0,
|
|
45
|
+
"danger": "#ff3b6f", "warn": "#f2d20c", "ok": "#36e07a", "info": "#2e9bff", "meta": "#2ee6e6", "note": "#ff7ae0"
|
|
46
|
+
},
|
|
47
|
+
"blue": {
|
|
48
|
+
"bg": "#04070b", "surface": "#0a1018", "text": "#2e9bff", "accent": "#2e9bff",
|
|
49
|
+
"fill": "#0a4c9c", "fill_text": "#000f22", "secondary": "#ff2e7a", "art": "#0d2a4a",
|
|
50
|
+
"accent_pressed": "#7cc2ff", "accent_text": "#ff2e7a",
|
|
51
|
+
"tag_bg": "#0a4c9c", "tag_fg": "#000f22", "tag_neutral_bg": "#0f1d2e", "tag_neutral_fg": "#2e9bff",
|
|
52
|
+
"shadow": "#000000", "shadow_strength": 2.0,
|
|
53
|
+
"danger": "#ff3b6f", "warn": "#f2d20c", "ok": "#36e07a", "info": "#2e9bff", "meta": "#2ee6e6", "note": "#ff7ae0"
|
|
54
|
+
},
|
|
55
|
+
"white": {
|
|
56
|
+
"bg": "#080808", "surface": "#111111", "text": "#e6e6e6", "accent": "#e6e6e6",
|
|
57
|
+
"fill": "#8a8a8a", "fill_text": "#0a0a0a", "secondary": "#2ee6e6", "art": "#2e2e2e",
|
|
58
|
+
"accent_pressed": "#ffffff", "accent_text": "#2ee6e6",
|
|
59
|
+
"tag_bg": "#8a8a8a", "tag_fg": "#0a0a0a", "tag_neutral_bg": "#222222", "tag_neutral_fg": "#e6e6e6",
|
|
60
|
+
"shadow": "#000000", "shadow_strength": 2.0,
|
|
61
|
+
"danger": "#ff3b6f", "warn": "#f2d20c", "ok": "#36e07a", "info": "#2e9bff", "meta": "#2ee6e6", "note": "#ff7ae0"
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
"""Token schema v1 — a design system as DATA, resolved headlessly (ruling
|
|
2
|
+
a439c226: one canonical tokens file per system, shared by the web and Qt
|
|
3
|
+
projections; DEC 0b1cfd81 fixed the shape).
|
|
4
|
+
|
|
5
|
+
A tokens file holds: `name`, `schema` (= 1), `fonts` (heading / body, the
|
|
6
|
+
optional mono and ui slots, the `files` globs registered at apply time),
|
|
7
|
+
`space`, `radius`, `type` (role -> [px, weight]), `ramps` (100..900 steps for
|
|
8
|
+
neutral and accent), `modes` (one semantic block per mode — a system's OWN
|
|
9
|
+
modes: Classical light / dark, Netrunner red / yellow / green / blue / white —
|
|
10
|
+
every block carrying the base semantics, the SIX STATE ROLES danger / warn /
|
|
11
|
+
ok / info / meta / note, and any system-specific keys that pass straight
|
|
12
|
+
through), the optional `scheme` map (OS light / dark -> a mode; absent = the
|
|
13
|
+
system does not follow the OS), the optional `reading` block (measure,
|
|
14
|
+
line_height — the reading-quality tokens), and the optional `icons`
|
|
15
|
+
declaration (set + stroke_width; Lucide at 1.5 by default). `@ramp.step`
|
|
16
|
+
references resolve against the ramps.
|
|
17
|
+
|
|
18
|
+
Nothing here imports Qt — which is why the schema lives in its own library
|
|
19
|
+
(design 0858bbd0): `check` and `resolve` run in tests, in the build CLIs and
|
|
20
|
+
on the web side, and the Qt kit reads the same module. `resolve` emits ONE
|
|
21
|
+
flat vocabulary — the QSS hole names ($bg $text $surface $accent $muted …)
|
|
22
|
+
plus the state roles, the typography slots and the scale — and every web
|
|
23
|
+
projection and kit widget reads that vocabulary (fork 1 of the design: no
|
|
24
|
+
second vocabulary, no aliases)."""
|
|
25
|
+
|
|
26
|
+
import json
|
|
27
|
+
import re
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
from typing import Any, Dict, Iterable, List, Optional, Tuple
|
|
30
|
+
|
|
31
|
+
SCHEMA_VERSION = 1
|
|
32
|
+
STATE_ROLES: Tuple[str, ...] = ("danger", "warn", "ok", "info", "meta", "note")
|
|
33
|
+
MODE_KEYS: Tuple[str, ...] = ("bg", "surface", "text", "accent", "accent_pressed", "accent_text",
|
|
34
|
+
"tag_bg", "tag_fg", "tag_neutral_bg", "tag_neutral_fg", "shadow")
|
|
35
|
+
TYPE_ROLES: Tuple[str, ...] = ("h1", "h2", "h3", "h4", "h5", "title", "kicker", "caption")
|
|
36
|
+
RAMPS: Tuple[str, ...] = ("neutral", "accent")
|
|
37
|
+
READING_DEFAULTS = {"measure": 68, "line_height": 1.45}
|
|
38
|
+
ICONS_DEFAULTS = {"set": "lucide", "stroke_width": 1.5}
|
|
39
|
+
|
|
40
|
+
_HEX = re.compile(r"^#[0-9a-fA-F]{6}$")
|
|
41
|
+
_REF = re.compile(r"^@([a-z_]+)\.([1-9]00)$")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class SchemaError(ValueError):
|
|
45
|
+
"""A tokens file that violates schema v1 — every problem listed, so one
|
|
46
|
+
read of the message fixes the file (the loud refusal the ruling asks for)."""
|
|
47
|
+
|
|
48
|
+
def __init__(self, source: str, problems: List[str]):
|
|
49
|
+
self.source = source
|
|
50
|
+
self.problems = list(problems)
|
|
51
|
+
super().__init__("%s: %d schema problem(s):\n - %s"
|
|
52
|
+
% (source, len(problems), "\n - ".join(problems)))
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# ---- color arithmetic (the CSS color-mix() equivalents) ---------------------
|
|
56
|
+
|
|
57
|
+
def _rgb(h: str) -> Tuple[int, int, int]:
|
|
58
|
+
h = h.lstrip("#")
|
|
59
|
+
return int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def to_hex(rgb: Iterable[float]) -> str:
|
|
63
|
+
return "#%02x%02x%02x" % tuple(max(0, min(255, round(c))) for c in rgb)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def rgba(h: str, a: float) -> str:
|
|
67
|
+
"""QSS rgba() with a 0-255 alpha (the form every Qt 6 version parses)."""
|
|
68
|
+
r, g, b = _rgb(h)
|
|
69
|
+
return f"rgba({r}, {g}, {b}, {round(max(0.0, min(1.0, a)) * 255)})"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def mix(fg: str, bg: str, t: float) -> str:
|
|
73
|
+
"""Flatten `fg` at opacity `t` over `bg` -> a solid hex (QPalette, painters,
|
|
74
|
+
rich-text HTML — anything that cannot take an rgba string)."""
|
|
75
|
+
f, b = _rgb(fg), _rgb(bg)
|
|
76
|
+
return to_hex(tuple(b[i] + (f[i] - b[i]) * t for i in range(3)))
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# ---- load / check -----------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
def load(path) -> Dict[str, Any]:
|
|
82
|
+
"""Read a tokens file (JSON). `check` it before resolving anything."""
|
|
83
|
+
return json.loads(Path(path).read_text(encoding="utf-8"))
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def slug(tokens: Dict[str, Any]) -> str:
|
|
87
|
+
"""The system's file-system / preference slug: lower-case, spaces -> dashes."""
|
|
88
|
+
return str(tokens.get("slug") or tokens.get("name", "system")).lower().replace(" ", "-")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def modes(tokens: Dict[str, Any]) -> List[str]:
|
|
92
|
+
return list((tokens.get("modes") or {}).keys())
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def mode_for_scheme(tokens: Dict[str, Any], scheme: str) -> Optional[str]:
|
|
96
|
+
"""The mode the system maps an OS scheme ("light" / "dark") onto — None
|
|
97
|
+
when the system declares no `scheme` map (it does not follow the OS)."""
|
|
98
|
+
m = tokens.get("scheme") or {}
|
|
99
|
+
mode = m.get(scheme)
|
|
100
|
+
return str(mode) if mode in (tokens.get("modes") or {}) else None
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _ref(tokens: Dict[str, Any], v):
|
|
104
|
+
"""Resolve '@accent.600' -> the ramp step (100..900); pass anything else."""
|
|
105
|
+
if isinstance(v, str) and v.startswith("@"):
|
|
106
|
+
ramp, step = v[1:].split(".")
|
|
107
|
+
return tokens["ramps"][ramp][int(step) // 100 - 1]
|
|
108
|
+
return v
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def check(tokens: Dict[str, Any], source: str = "<tokens>") -> None:
|
|
112
|
+
"""Refuse a malformed system loudly: every missing / mistyped field named
|
|
113
|
+
in one SchemaError. A tokens file that passes here resolves for every
|
|
114
|
+
mode it declares."""
|
|
115
|
+
p: List[str] = []
|
|
116
|
+
if not isinstance(tokens, dict):
|
|
117
|
+
raise SchemaError(source, ["the tokens file must be a JSON object"])
|
|
118
|
+
if tokens.get("schema") != SCHEMA_VERSION:
|
|
119
|
+
p.append("schema: expected %d, got %r" % (SCHEMA_VERSION, tokens.get("schema")))
|
|
120
|
+
if not isinstance(tokens.get("name"), str) or not tokens.get("name"):
|
|
121
|
+
p.append("name: a non-empty string is required")
|
|
122
|
+
fonts = tokens.get("fonts")
|
|
123
|
+
if not isinstance(fonts, dict):
|
|
124
|
+
p.append("fonts: an object with heading and body slots is required")
|
|
125
|
+
fonts = {}
|
|
126
|
+
for slot in ("heading", "body"):
|
|
127
|
+
f = fonts.get(slot)
|
|
128
|
+
if not isinstance(f, dict) or not isinstance(f.get("family"), str):
|
|
129
|
+
p.append("fonts.%s.family: a string is required" % slot)
|
|
130
|
+
if isinstance(fonts.get("heading"), dict) and not isinstance(fonts["heading"].get("weight"), int):
|
|
131
|
+
p.append("fonts.heading.weight: an integer is required")
|
|
132
|
+
if isinstance(fonts.get("body"), dict) and not isinstance(fonts["body"].get("size"), (int, float)):
|
|
133
|
+
p.append("fonts.body.size: a pixel size is required")
|
|
134
|
+
for slot in ("mono", "ui"):
|
|
135
|
+
f = fonts.get(slot)
|
|
136
|
+
if f is not None and (not isinstance(f, dict) or not isinstance(f.get("family", ""), str)):
|
|
137
|
+
p.append("fonts.%s: an object with a family string when present" % slot)
|
|
138
|
+
if fonts.get("files") is not None and not (isinstance(fonts["files"], list)
|
|
139
|
+
and all(isinstance(x, str) for x in fonts["files"])):
|
|
140
|
+
p.append("fonts.files: a list of glob strings when present")
|
|
141
|
+
for scale in ("space", "radius"):
|
|
142
|
+
d = tokens.get(scale)
|
|
143
|
+
if not isinstance(d, dict) or not d or not all(isinstance(v, (int, float)) for v in d.values()):
|
|
144
|
+
p.append("%s: a non-empty object of pixel numbers is required" % scale)
|
|
145
|
+
typ = tokens.get("type")
|
|
146
|
+
if not isinstance(typ, dict):
|
|
147
|
+
p.append("type: an object of role -> [px, weight] is required")
|
|
148
|
+
typ = {}
|
|
149
|
+
for role in TYPE_ROLES:
|
|
150
|
+
if role not in typ:
|
|
151
|
+
p.append("type.%s: required" % role)
|
|
152
|
+
for role, val in typ.items():
|
|
153
|
+
if not (isinstance(val, list) and len(val) == 2
|
|
154
|
+
and all(isinstance(x, (int, float)) for x in val)):
|
|
155
|
+
p.append("type.%s: expected [px, weight]" % role)
|
|
156
|
+
ramps = tokens.get("ramps")
|
|
157
|
+
if not isinstance(ramps, dict):
|
|
158
|
+
p.append("ramps: an object with neutral and accent 9-step ramps is required")
|
|
159
|
+
ramps = {}
|
|
160
|
+
for name in RAMPS:
|
|
161
|
+
r = ramps.get(name)
|
|
162
|
+
if not (isinstance(r, list) and len(r) == 9 and all(isinstance(x, str) and _HEX.match(x) for x in r)):
|
|
163
|
+
p.append("ramps.%s: exactly nine #rrggbb steps (100..900) are required" % name)
|
|
164
|
+
mode_map = tokens.get("modes")
|
|
165
|
+
if not isinstance(mode_map, dict) or not mode_map:
|
|
166
|
+
p.append("modes: at least one mode block is required")
|
|
167
|
+
mode_map = {}
|
|
168
|
+
for mode, block in mode_map.items():
|
|
169
|
+
if not isinstance(block, dict):
|
|
170
|
+
p.append("modes.%s: expected an object" % mode)
|
|
171
|
+
continue
|
|
172
|
+
for key in MODE_KEYS + STATE_ROLES:
|
|
173
|
+
val = block.get(key)
|
|
174
|
+
if val is None:
|
|
175
|
+
p.append("modes.%s.%s: required%s" % (mode, key,
|
|
176
|
+
" (a state role — every mode carries all six)" if key in STATE_ROLES else ""))
|
|
177
|
+
elif not isinstance(val, str) or not (_HEX.match(val) or _REF.match(val)):
|
|
178
|
+
p.append("modes.%s.%s: expected #rrggbb or @ramp.step, got %r" % (mode, key, val))
|
|
179
|
+
elif _REF.match(val):
|
|
180
|
+
ramp = _REF.match(val).group(1)
|
|
181
|
+
if ramp not in ramps or not isinstance(ramps.get(ramp), list) or len(ramps[ramp]) != 9:
|
|
182
|
+
p.append("modes.%s.%s: %s references a ramp that does not exist" % (mode, key, val))
|
|
183
|
+
ss = block.get("shadow_strength", 1.0)
|
|
184
|
+
if not isinstance(ss, (int, float)):
|
|
185
|
+
p.append("modes.%s.shadow_strength: expected a number" % mode)
|
|
186
|
+
scheme = tokens.get("scheme")
|
|
187
|
+
if scheme is not None:
|
|
188
|
+
if not isinstance(scheme, dict) or set(scheme) - {"light", "dark"}:
|
|
189
|
+
p.append("scheme: an object with light and/or dark keys naming modes")
|
|
190
|
+
else:
|
|
191
|
+
for k, v in scheme.items():
|
|
192
|
+
if v not in mode_map:
|
|
193
|
+
p.append("scheme.%s: %r is not a declared mode" % (k, v))
|
|
194
|
+
reading = tokens.get("reading")
|
|
195
|
+
if reading is not None:
|
|
196
|
+
if not isinstance(reading, dict):
|
|
197
|
+
p.append("reading: an object with measure / line_height")
|
|
198
|
+
else:
|
|
199
|
+
if "measure" in reading and not isinstance(reading["measure"], int):
|
|
200
|
+
p.append("reading.measure: an integer (characters)")
|
|
201
|
+
if "line_height" in reading and not isinstance(reading["line_height"], (int, float)):
|
|
202
|
+
p.append("reading.line_height: a number (ratio)")
|
|
203
|
+
icons = tokens.get("icons")
|
|
204
|
+
if icons is not None:
|
|
205
|
+
if not isinstance(icons, dict) or not isinstance(icons.get("set", "lucide"), str):
|
|
206
|
+
p.append("icons: an object with a set name (and an optional stroke_width)")
|
|
207
|
+
elif "stroke_width" in icons and not isinstance(icons["stroke_width"], (int, float)):
|
|
208
|
+
p.append("icons.stroke_width: a number")
|
|
209
|
+
chrome = tokens.get("chrome")
|
|
210
|
+
if chrome is not None:
|
|
211
|
+
if not isinstance(chrome, dict):
|
|
212
|
+
p.append("chrome: an object naming the shell chrome's ink (titlebar_ink: a mode key)")
|
|
213
|
+
else:
|
|
214
|
+
ink = chrome.get("titlebar_ink", "text")
|
|
215
|
+
if not isinstance(ink, str) or any(ink not in (block or {}) and ink not in ("text", "accent")
|
|
216
|
+
for block in mode_map.values() if isinstance(block, dict)):
|
|
217
|
+
p.append("chrome.titlebar_ink: must name a key every mode carries (e.g. fill_text)")
|
|
218
|
+
if p:
|
|
219
|
+
raise SchemaError(source, p)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
# ---- resolve ----------------------------------------------------------------
|
|
223
|
+
|
|
224
|
+
def resolve(tokens: Dict[str, Any], mode: Optional[str] = None) -> Dict[str, str]:
|
|
225
|
+
"""tokens + mode -> the flat vocabulary every projection reads: the QSS
|
|
226
|
+
holes (colors, tints, solids, radii, spacing, type scale), the six state
|
|
227
|
+
roles + `dim`, the four font slots with pixel sizes, and the reading
|
|
228
|
+
tokens. `mode` None = the system's first mode."""
|
|
229
|
+
mode = mode or modes(tokens)[0]
|
|
230
|
+
m = {k: _ref(tokens, v) for k, v in tokens["modes"][mode].items()}
|
|
231
|
+
bg, text, acc = m["bg"], m["text"], m["accent"]
|
|
232
|
+
f = tokens["fonts"]
|
|
233
|
+
body = f["body"]
|
|
234
|
+
mono = f.get("mono") or {}
|
|
235
|
+
ui = f.get("ui") or {}
|
|
236
|
+
reading = {**READING_DEFAULTS, **(tokens.get("reading") or {})}
|
|
237
|
+
icons = {**ICONS_DEFAULTS, **(tokens.get("icons") or {})}
|
|
238
|
+
v: Dict[str, str] = {
|
|
239
|
+
"system": slug(tokens), "name": str(tokens["name"]), "mode": mode,
|
|
240
|
+
"bg": bg, "surface": m["surface"], "text": text,
|
|
241
|
+
"accent": acc, "accent_pressed": m["accent_pressed"], "accent_text": m["accent_text"],
|
|
242
|
+
"tag_bg": m["tag_bg"], "tag_fg": m["tag_fg"],
|
|
243
|
+
"tag_neutral_bg": m["tag_neutral_bg"], "tag_neutral_fg": m["tag_neutral_fg"],
|
|
244
|
+
# ink tints (color-mix(text N%, transparent) in the CSS)
|
|
245
|
+
"divider": rgba(text, .16), "divider_strong": rgba(text, .30), "line": rgba(text, .55),
|
|
246
|
+
"border_hover": rgba(text, .45), "muted": rgba(text, .55),
|
|
247
|
+
"label": rgba(text, .70), "header_text": rgba(text, .60),
|
|
248
|
+
"text_hover": rgba(text, .07), "text_pressed": rgba(text, .14),
|
|
249
|
+
"row_hover": rgba(text, .04), "row_alt": rgba(text, .025),
|
|
250
|
+
"disabled_text": rgba(text, .35), "disabled_border": rgba(text, .10),
|
|
251
|
+
"scroll_handle": rgba(text, .18), "scroll_handle_hover": rgba(text, .34),
|
|
252
|
+
# accent tints
|
|
253
|
+
"accent_hover": rgba(acc, .12), "accent_press_tint": rgba(acc, .22),
|
|
254
|
+
"ghost_hover": rgba(acc, .10), "ghost_press": rgba(acc, .18),
|
|
255
|
+
"selection": rgba(acc, .30),
|
|
256
|
+
# solid equivalents for QPalette / painting / rich-text HTML
|
|
257
|
+
"divider_solid": mix(text, bg, .16), "divider_strong_solid": mix(text, bg, .30),
|
|
258
|
+
"line_solid": mix(text, bg, .55),
|
|
259
|
+
"muted_solid": mix(text, bg, .55), "label_solid": mix(text, bg, .70),
|
|
260
|
+
"selection_solid": mix(acc, bg, .30), "row_alt_solid": mix(text, bg, .025),
|
|
261
|
+
"disabled_solid": mix(text, bg, .35),
|
|
262
|
+
"backdrop": rgba(tokens["ramps"]["neutral"][8], .5),
|
|
263
|
+
"shadow": m["shadow"], "shadow_strength": str(m.get("shadow_strength", 1.0)),
|
|
264
|
+
# the state channel: six roles + dim (muted text, the seventh word the spines use)
|
|
265
|
+
**{role: m[role] for role in STATE_ROLES},
|
|
266
|
+
"dim": mix(text, bg, .55),
|
|
267
|
+
# typography slots (pixel sizes — Qt setPixelSize, CSS px)
|
|
268
|
+
"font_heading": f["heading"]["family"], "heading_weight": str(f["heading"]["weight"]),
|
|
269
|
+
"font_body": body["family"], "fs_body": str(body["size"]),
|
|
270
|
+
"body_weight": str(body.get("weight", 400)),
|
|
271
|
+
"font_mono": str(mono.get("family", "")), "fs_mono": str(mono.get("size", max(1, body["size"] - 1))),
|
|
272
|
+
"font_ui": str(ui.get("family") or body["family"]), "fs_ui": str(ui.get("size", body["size"])),
|
|
273
|
+
# reading
|
|
274
|
+
"measure": str(reading["measure"]), "line_height": str(reading["line_height"]),
|
|
275
|
+
# icons
|
|
276
|
+
"icon_set": str(icons["set"]), "icon_stroke": str(icons["stroke_width"]),
|
|
277
|
+
}
|
|
278
|
+
for k, val in m.items(): # system-specific mode keys (e.g. fill, secondary) pass through
|
|
279
|
+
v.setdefault(k, str(val))
|
|
280
|
+
if isinstance(val, str) and val.startswith("#"):
|
|
281
|
+
v.setdefault(f"{k}_hover", rgba(val, .14))
|
|
282
|
+
# the shell chrome's ink (ruling d1e3043e): the title bar's glyphs are
|
|
283
|
+
# painted pixmaps, so the system names which mode key inks them — the
|
|
284
|
+
# text by default, a bar's own contrast key (Netrunner: fill_text) else
|
|
285
|
+
chrome = tokens.get("chrome") or {}
|
|
286
|
+
v["titlebar_ink"] = v.get(str(chrome.get("titlebar_ink", "text")), text)
|
|
287
|
+
for k, px in tokens["radius"].items():
|
|
288
|
+
v[f"radius_{k}"] = str(round(px))
|
|
289
|
+
for k, px in tokens["space"].items():
|
|
290
|
+
v[f"space_{k}"] = str(round(px))
|
|
291
|
+
for k, (size, weight) in tokens["type"].items():
|
|
292
|
+
v[f"fs_{k}"], v[f"fw_{k}"] = str(size), str(weight)
|
|
293
|
+
return v
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
# ---- the web projection -----------------------------------------------------
|
|
297
|
+
|
|
298
|
+
def to_css(tokens: Dict[str, Any], selector: str = ":root") -> str:
|
|
299
|
+
"""The CSS custom-properties layer for a system — the ruling's second
|
|
300
|
+
consumer (the Quarto site 8079ae0f derives its theme from this, never from
|
|
301
|
+
Bootswatch). The first mode lands on `selector`; every other mode lands on
|
|
302
|
+
`selector[data-mode="<mode>"]`, so a page switches modes by one attribute.
|
|
303
|
+
Ramps, spacing, radii and the type scale are mode-independent and emitted
|
|
304
|
+
once. Variable names mirror the resolved vocabulary with dashes."""
|
|
305
|
+
out: List[str] = []
|
|
306
|
+
first = modes(tokens)[0]
|
|
307
|
+
static: List[str] = []
|
|
308
|
+
for ramp, steps in tokens["ramps"].items():
|
|
309
|
+
static += [f" --{ramp}-{(i + 1) * 100}: {c};" for i, c in enumerate(steps)]
|
|
310
|
+
static += [f" --space-{k}: {v}px;" for k, v in tokens["space"].items()]
|
|
311
|
+
static += [f" --radius-{k}: {v}px;" for k, v in tokens["radius"].items()]
|
|
312
|
+
static += [f" --fs-{k}: {s}px;\n --fw-{k}: {w};" for k, (s, w) in tokens["type"].items()]
|
|
313
|
+
f = tokens["fonts"]
|
|
314
|
+
static += [f' --font-heading: "{f["heading"]["family"]}";',
|
|
315
|
+
f' --heading-weight: {f["heading"]["weight"]};',
|
|
316
|
+
f' --font-body: "{f["body"]["family"]}";',
|
|
317
|
+
f' --fs-body: {f["body"]["size"]}px;']
|
|
318
|
+
if f.get("mono", {}).get("family"):
|
|
319
|
+
static.append(f' --font-mono: "{f["mono"]["family"]}";')
|
|
320
|
+
reading = {**READING_DEFAULTS, **(tokens.get("reading") or {})}
|
|
321
|
+
static += [f" --measure: {reading['measure']}ch;", f" --line-height: {reading['line_height']};"]
|
|
322
|
+
per_mode = tuple(MODE_KEYS) + STATE_ROLES + ("dim", "muted", "divider", "divider_strong", "line",
|
|
323
|
+
"label", "selection", "accent_hover", "row_hover")
|
|
324
|
+
for mode in modes(tokens):
|
|
325
|
+
v = resolve(tokens, mode)
|
|
326
|
+
keys = list(per_mode) + [k for k in tokens["modes"][mode] if k not in per_mode and k in v]
|
|
327
|
+
body = [f" --{k.replace('_', '-')}: {v[k]};" for k in keys if k != "shadow_strength"]
|
|
328
|
+
body.append(f" --shadow-strength: {v['shadow_strength']};")
|
|
329
|
+
sel = selector if mode == first else f'{selector}[data-mode="{mode}"]'
|
|
330
|
+
block = body if mode != first else static + body
|
|
331
|
+
out.append("%s {\n%s\n}" % (sel, "\n".join(block)))
|
|
332
|
+
return "/* %s — design tokens, projected by cjm-design-system (schema v%d) */\n%s\n" % (
|
|
333
|
+
tokens["name"], SCHEMA_VERSION, "\n\n".join(out))
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Build the static web projections of a design system: the CSS
|
|
2
|
+
custom-properties layer (`tokens.to_css`, the web consumer 8079ae0f), after
|
|
3
|
+
the schema check. The Qt projections (QSS per mode, recolored icons) build
|
|
4
|
+
from the kit (`python -m cjm_substrate_qt_kit.tools.build`).
|
|
5
|
+
|
|
6
|
+
python -m cjm_design_system.tools.build netrunner -o build/
|
|
7
|
+
python -m cjm_design_system.tools.build path/to/mysystem/tokens.json --check
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from .. import systems, tokens as T
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def main(argv=None) -> int:
|
|
17
|
+
ap = argparse.ArgumentParser(prog="python -m cjm_design_system.tools.build")
|
|
18
|
+
ap.add_argument("system", help="a vendored slug or a tokens.json path")
|
|
19
|
+
ap.add_argument("-o", "--out", default="build")
|
|
20
|
+
ap.add_argument("--check", action="store_true", help="validate only; write nothing")
|
|
21
|
+
a = ap.parse_args(argv)
|
|
22
|
+
path = systems.tokens_path(a.system)
|
|
23
|
+
tok = T.load(path)
|
|
24
|
+
T.check(tok, str(path))
|
|
25
|
+
if a.check:
|
|
26
|
+
print(f"{T.slug(tok)}: schema v{T.SCHEMA_VERSION} OK — modes {T.modes(tok)}")
|
|
27
|
+
return 0
|
|
28
|
+
out = Path(a.out)
|
|
29
|
+
out.mkdir(parents=True, exist_ok=True)
|
|
30
|
+
css = out / f"{T.slug(tok)}.css"
|
|
31
|
+
css.write_text(T.to_css(tok), encoding="utf-8")
|
|
32
|
+
print(f"wrote {css}")
|
|
33
|
+
return 0
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
if __name__ == "__main__":
|
|
37
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""Import a web-first design system's CSS into a schema-v1 tokens file.
|
|
2
|
+
|
|
3
|
+
python -m cjm_design_system.tools.css_to_tokens styles.css -o mysystem/tokens.json [--name "My System"]
|
|
4
|
+
|
|
5
|
+
Reads the `:root { --color-*; --font-*; --space-*; --radius-* }` block (the
|
|
6
|
+
shape a Claude Design handoff uses) and writes light-mode tokens straight
|
|
7
|
+
from it; dark mode is DERIVED from the 100-900 ramps and the six state roles
|
|
8
|
+
are seeded from the reading-tuned set — review both by eye in the
|
|
9
|
+
gallery (a derivation is a starting point, never a verdict; ruling a439c226:
|
|
10
|
+
the CSS is an import source, the tokens file is the source of truth). The
|
|
11
|
+
output passes `tokens.check` or the tool refuses to write it."""
|
|
12
|
+
|
|
13
|
+
import argparse
|
|
14
|
+
import json
|
|
15
|
+
import re
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
from .. import tokens as T
|
|
19
|
+
|
|
20
|
+
VAR = re.compile(r"--([\w-]+)\s*:\s*([^;]+);")
|
|
21
|
+
HEX = re.compile(r"#[0-9a-fA-F]{6}")
|
|
22
|
+
|
|
23
|
+
STATE_LIGHT = {"danger": "#a4382a", "warn": "#8c5e14", "ok": "#3a7a3a",
|
|
24
|
+
"info": "#4a5f96", "meta": "#2a6f7c", "note": "#7a4a8a"}
|
|
25
|
+
STATE_DARK = {"danger": "#dc8070", "warn": "#d0a552", "ok": "#7dbb7d",
|
|
26
|
+
"info": "#8fa6d6", "meta": "#67b2bf", "note": "#bb93c9"}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def parse_root(css: str) -> dict:
|
|
30
|
+
css = re.sub(r"/\*.*?\*/", "", css, flags=re.S)
|
|
31
|
+
m = re.search(r":root\s*{(.*?)}", css, flags=re.S)
|
|
32
|
+
if not m:
|
|
33
|
+
raise SystemExit("no :root block found")
|
|
34
|
+
return {k: v.strip() for k, v in VAR.findall(m.group(1))}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def family(v: str) -> str:
|
|
38
|
+
return v.split(",")[0].strip().strip("'\"")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def px(v: str) -> float:
|
|
42
|
+
return float(re.sub(r"[^\d.]", "", v) or 0)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def ramp(vars_: dict, role: str):
|
|
46
|
+
steps = [vars_.get(f"color-{role}-{s}") for s in range(100, 1000, 100)]
|
|
47
|
+
return steps if all(steps) else None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def darker(h: str, k: float) -> str:
|
|
51
|
+
r, g, b = (int(h[i:i + 2], 16) for i in (1, 3, 5))
|
|
52
|
+
return "#%02x%02x%02x" % tuple(round(c * k) for c in (r, g, b))
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def convert(css: str, name: str) -> dict:
|
|
56
|
+
v = parse_root(css)
|
|
57
|
+
col = lambda k: (HEX.findall(v.get(f"color-{k}", "")) or [None])[0] # noqa: E731
|
|
58
|
+
neutral, accent = ramp(v, "neutral"), ramp(v, "accent")
|
|
59
|
+
if not (neutral and accent):
|
|
60
|
+
raise SystemExit("need --color-neutral-100..900 and --color-accent-100..900 ramps")
|
|
61
|
+
hw = int(px(v.get("font-heading-weight", "600")))
|
|
62
|
+
heading, body = family(v["font-heading"]), family(v["font-body"])
|
|
63
|
+
mono = family(v["font-mono"]) if v.get("font-mono") else ""
|
|
64
|
+
light_ground = int(col("bg")[1:3], 16) > 128
|
|
65
|
+
light = {
|
|
66
|
+
"bg": col("bg"), "surface": col("surface"), "text": col("text"), "accent": col("accent"),
|
|
67
|
+
"accent_pressed": "@accent.600", "accent_text": "@accent.700",
|
|
68
|
+
"tag_bg": "@accent.100", "tag_fg": "@accent.800",
|
|
69
|
+
"tag_neutral_bg": "@neutral.100", "tag_neutral_fg": "@neutral.800",
|
|
70
|
+
"shadow": "@neutral.900", "shadow_strength": 1.0, **STATE_LIGHT,
|
|
71
|
+
}
|
|
72
|
+
dark = {
|
|
73
|
+
"bg": darker(neutral[8], .68), "surface": darker(neutral[8], .86), "text": neutral[0],
|
|
74
|
+
"accent": accent[4] if light_ground else col("accent"),
|
|
75
|
+
"accent_pressed": "@accent.400", "accent_text": "@accent.300",
|
|
76
|
+
"tag_bg": "@accent.900", "tag_fg": "@accent.200",
|
|
77
|
+
"tag_neutral_bg": "@neutral.900", "tag_neutral_fg": "@neutral.200",
|
|
78
|
+
"shadow": "#000000", "shadow_strength": 2.2, **STATE_DARK,
|
|
79
|
+
}
|
|
80
|
+
modes = {"light": light, "dark": dark} if light_ground else {"dark": light}
|
|
81
|
+
out = {
|
|
82
|
+
"schema": T.SCHEMA_VERSION,
|
|
83
|
+
"name": name,
|
|
84
|
+
"fonts": {
|
|
85
|
+
"heading": {"family": heading, "weight": hw},
|
|
86
|
+
"body": {"family": body, "size": 15},
|
|
87
|
+
"mono": {"family": mono, "size": 13},
|
|
88
|
+
"files": [heading.replace(" ", "") + "*.ttf", body.replace(" ", "") + "*.ttf"],
|
|
89
|
+
},
|
|
90
|
+
"space": {k.split("-")[1]: px(x) for k, x in v.items() if re.fullmatch(r"space-\d+", k)},
|
|
91
|
+
"radius": {k.split("-")[1]: px(x) for k, x in v.items() if re.fullmatch(r"radius-\w+", k)},
|
|
92
|
+
"type": {"h1": [42, 400], "h2": [32, 400], "h3": [25, hw], "h4": [20, hw], "h5": [16, hw],
|
|
93
|
+
"title": [17, hw], "kicker": [10, 400], "caption": [11, 400]},
|
|
94
|
+
"ramps": {"neutral": neutral, "accent": accent},
|
|
95
|
+
"scheme": {"light": "light", "dark": "dark"} if light_ground else {"dark": "dark"},
|
|
96
|
+
"reading": dict(T.READING_DEFAULTS),
|
|
97
|
+
"icons": dict(T.ICONS_DEFAULTS),
|
|
98
|
+
"modes": modes,
|
|
99
|
+
}
|
|
100
|
+
if not out["space"]:
|
|
101
|
+
out["space"] = {"1": 4, "2": 8, "3": 12, "4": 16, "6": 24, "8": 32}
|
|
102
|
+
if not out["radius"]:
|
|
103
|
+
out["radius"] = {"sm": 2, "md": 4, "lg": 8}
|
|
104
|
+
T.check(out, f"<converted from CSS: {name}>")
|
|
105
|
+
return out
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def main(argv=None) -> int:
|
|
109
|
+
ap = argparse.ArgumentParser(prog="python -m cjm_design_system.tools.css_to_tokens")
|
|
110
|
+
ap.add_argument("css")
|
|
111
|
+
ap.add_argument("-o", "--out", required=True)
|
|
112
|
+
ap.add_argument("--name")
|
|
113
|
+
a = ap.parse_args(argv)
|
|
114
|
+
name = a.name or Path(a.out).parent.name.title()
|
|
115
|
+
out = convert(Path(a.css).read_text(encoding="utf-8"), name)
|
|
116
|
+
Path(a.out).parent.mkdir(parents=True, exist_ok=True)
|
|
117
|
+
Path(a.out).write_text(json.dumps(out, indent=2) + "\n", encoding="utf-8")
|
|
118
|
+
print(f"wrote {a.out} modes={list(out['modes'])} heading={out['fonts']['heading']['family']}")
|
|
119
|
+
return 0
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
if __name__ == "__main__":
|
|
123
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cjm-design-system
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: A design system as data (ruling a439c226): token schema v1 with its check and its headless resolution into one flat vocabulary, the CSS custom-properties projection for the web, the vendored seed systems (Classical, Netrunner) with their fonts, and the importer for a web-first system's CSS. No Qt: the Qt kit (cjm-substrate-qt-kit) consumes the same tokens. Born on-graph.
|
|
5
|
+
Author-email: "Christian J. Mills" <9126128+cj-mills@users.noreply.github.com>
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
Project-URL: Repository, https://github.com/cj-mills/cjm-design-system
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
10
|
+
Requires-Python: >=3.12
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# cjm-design-system
|
|
14
|
+
|
|
15
|
+
<!-- generated from the context graph by `cjm-context-graph readme` — do not edit by hand; edit the graph (the urge to hand-edit = move it on-graph) -->
|
|
16
|
+
|
|
17
|
+
A design system as data (ruling a439c226): token schema v1 with its check and its headless resolution into one flat vocabulary, the CSS custom-properties projection for the web, the vendored seed systems Classical and Netrunner with their OFL fonts, and the importer for a web-first system's CSS. Nothing here imports Qt, so a web build never needs PySide6; the Qt kit (cjm-substrate-qt-kit) consumes the same tokens and keeps only what Qt reads. Born out of the kit by design 0858bbd0, the theme half of the public-site redesign build 8079ae0f.
|
|
18
|
+
|
|
19
|
+
## Modules
|
|
20
|
+
|
|
21
|
+
- **`cjm_design_system.__init__`** — A design system as DATA (ruling a439c226): one canonical tokens file per
|
|
22
|
+
- **`cjm_design_system.systems.__init__`** — The design systems this library ships as DATA (ruling a439c226): one
|
|
23
|
+
- **`cjm_design_system.tokens`** — Token schema v1 — a design system as DATA, resolved headlessly (ruling
|
|
24
|
+
- **`cjm_design_system.tools.__init__`** — Design-system tools (run as modules):
|
|
25
|
+
- **`cjm_design_system.tools.build`** — Build the static web projections of a design system: the CSS
|
|
26
|
+
- **`cjm_design_system.tools.css_to_tokens`** — Import a web-first design system's CSS into a schema-v1 tokens file.
|
|
27
|
+
|
|
28
|
+
## API
|
|
29
|
+
|
|
30
|
+
### `cjm_design_system.systems.__init__`
|
|
31
|
+
|
|
32
|
+
- `available` _function_ — Every vendored system slug (a directory carrying a tokens.json).
|
|
33
|
+
- `locate` _function_ — The directory of a system: a vendored slug, a directory holding a
|
|
34
|
+
- `tokens_path` _function_
|
|
35
|
+
|
|
36
|
+
### `cjm_design_system.tokens`
|
|
37
|
+
|
|
38
|
+
- `SchemaError` _class_ — A tokens file that violates schema v1 — every problem listed, so one
|
|
39
|
+
- `check` _function_ — Refuse a malformed system loudly: every missing / mistyped field named
|
|
40
|
+
- `load` _function_ — Read a tokens file (JSON). `check` it before resolving anything.
|
|
41
|
+
- `mix` _function_ — Flatten `fg` at opacity `t` over `bg` -> a solid hex (QPalette, painters,
|
|
42
|
+
- `mode_for_scheme` _function_ — The mode the system maps an OS scheme ("light" / "dark") onto — None
|
|
43
|
+
- `modes` _function_
|
|
44
|
+
- `resolve` _function_ — tokens + mode -> the flat vocabulary every projection reads: the QSS
|
|
45
|
+
- `rgba` _function_ — QSS rgba() with a 0-255 alpha (the form every Qt 6 version parses).
|
|
46
|
+
- `slug` _function_ — The system's file-system / preference slug: lower-case, spaces -> dashes.
|
|
47
|
+
- `to_css` _function_ — The CSS custom-properties layer for a system — the ruling's second
|
|
48
|
+
- `to_hex` _function_
|
|
49
|
+
|
|
50
|
+
### `cjm_design_system.tools.build`
|
|
51
|
+
|
|
52
|
+
- `main` _function_
|
|
53
|
+
|
|
54
|
+
### `cjm_design_system.tools.css_to_tokens`
|
|
55
|
+
|
|
56
|
+
- `convert` _function_
|
|
57
|
+
- `darker` _function_
|
|
58
|
+
- `family` _function_
|
|
59
|
+
- `main` _function_
|
|
60
|
+
- `parse_root` _function_
|
|
61
|
+
- `px` _function_
|
|
62
|
+
- `ramp` _function_
|
|
63
|
+
|
|
64
|
+
## Dependencies
|
|
65
|
+
|
|
66
|
+
**Used by:** `cjm-context-graph-projection`, `cjm-substrate-qt-kit`
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
cjm_design_system/__init__.py
|
|
4
|
+
cjm_design_system/tokens.py
|
|
5
|
+
cjm_design_system.egg-info/PKG-INFO
|
|
6
|
+
cjm_design_system.egg-info/SOURCES.txt
|
|
7
|
+
cjm_design_system.egg-info/dependency_links.txt
|
|
8
|
+
cjm_design_system.egg-info/top_level.txt
|
|
9
|
+
cjm_design_system/systems/__init__.py
|
|
10
|
+
cjm_design_system/systems/classical/tokens.json
|
|
11
|
+
cjm_design_system/systems/classical/fonts/CormorantGaramond-Italic[wght].ttf
|
|
12
|
+
cjm_design_system/systems/classical/fonts/CormorantGaramond[wght].ttf
|
|
13
|
+
cjm_design_system/systems/classical/fonts/Lora-Italic[wght].ttf
|
|
14
|
+
cjm_design_system/systems/classical/fonts/Lora[wght].ttf
|
|
15
|
+
cjm_design_system/systems/classical/fonts/OFL.txt
|
|
16
|
+
cjm_design_system/systems/netrunner/tokens.json
|
|
17
|
+
cjm_design_system/systems/netrunner/fonts/CourierPrime-Bold.ttf
|
|
18
|
+
cjm_design_system/systems/netrunner/fonts/CourierPrime-Italic.ttf
|
|
19
|
+
cjm_design_system/systems/netrunner/fonts/CourierPrime-Regular.ttf
|
|
20
|
+
cjm_design_system/systems/netrunner/fonts/OFL.txt
|
|
21
|
+
cjm_design_system/systems/netrunner/fonts/Silkscreen-Bold.ttf
|
|
22
|
+
cjm_design_system/systems/netrunner/fonts/Silkscreen-Regular.ttf
|
|
23
|
+
cjm_design_system/tools/__init__.py
|
|
24
|
+
cjm_design_system/tools/build.py
|
|
25
|
+
cjm_design_system/tools/css_to_tokens.py
|
|
26
|
+
tests/test_tokens.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
cjm_design_system
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=64"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "cjm-design-system"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "A design system as data (ruling a439c226): token schema v1 with its check and its headless resolution into one flat vocabulary, the CSS custom-properties projection for the web, the vendored seed systems (Classical, Netrunner) with their fonts, and the importer for a web-first system's CSS. No Qt: the Qt kit (cjm-substrate-qt-kit) consumes the same tokens. Born on-graph."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.12"
|
|
11
|
+
license = {text = "Apache-2.0"}
|
|
12
|
+
authors = [{name = "Christian J. Mills", email = "9126128+cj-mills@users.noreply.github.com"}]
|
|
13
|
+
classifiers = ["Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only"]
|
|
14
|
+
dependencies = []
|
|
15
|
+
|
|
16
|
+
[project.urls]
|
|
17
|
+
Repository = "https://github.com/cj-mills/cjm-design-system"
|
|
18
|
+
|
|
19
|
+
[tool.setuptools.dynamic]
|
|
20
|
+
version = {attr = "cjm_design_system.__version__"}
|
|
21
|
+
|
|
22
|
+
[tool.setuptools.packages.find]
|
|
23
|
+
include = ["cjm_design_system*"]
|
|
24
|
+
|
|
25
|
+
[tool.setuptools.package-data]
|
|
26
|
+
cjm_design_system = ["systems/*/tokens.json", "systems/*/fonts/*.ttf", "systems/*/fonts/*.otf",
|
|
27
|
+
"systems/*/fonts/OFL.txt"]
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Schema v1 contract: both vendored systems pass `check`; a malformed system
|
|
2
|
+
is refused loudly with every problem named; `resolve` emits the one
|
|
3
|
+
vocabulary (holes, state roles, dim, typography slots, scale) headlessly;
|
|
4
|
+
`to_css` projects the same tokens for the web side."""
|
|
5
|
+
|
|
6
|
+
import copy
|
|
7
|
+
import json
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
from cjm_design_system import systems
|
|
12
|
+
from cjm_design_system import tokens as T
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@pytest.fixture(params=["classical", "netrunner"])
|
|
16
|
+
def system(request):
|
|
17
|
+
return request.param, T.load(systems.tokens_path(request.param))
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_vendored_systems_pass_check(system):
|
|
21
|
+
slug, tok = system
|
|
22
|
+
T.check(tok, slug)
|
|
23
|
+
assert T.slug(tok) == slug
|
|
24
|
+
assert len(T.modes(tok)) >= 2
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_check_refuses_loudly_naming_every_problem():
|
|
28
|
+
tok = T.load(systems.tokens_path("classical"))
|
|
29
|
+
bad = copy.deepcopy(tok)
|
|
30
|
+
del bad["modes"]["light"]["danger"] # a state role missing
|
|
31
|
+
bad["modes"]["dark"]["accent"] = "red" # not a hex / ref
|
|
32
|
+
bad["ramps"]["accent"] = bad["ramps"]["accent"][:5] # short ramp
|
|
33
|
+
bad["scheme"]["dark"] = "midnight" # names a mode that does not exist
|
|
34
|
+
bad["schema"] = 2
|
|
35
|
+
with pytest.raises(T.SchemaError) as e:
|
|
36
|
+
T.check(bad, "bad.json")
|
|
37
|
+
msg = str(e.value)
|
|
38
|
+
assert "bad.json" in msg
|
|
39
|
+
for needle in ("modes.light.danger", "modes.dark.accent", "ramps.accent", "scheme.dark", "schema:"):
|
|
40
|
+
assert needle in msg, needle
|
|
41
|
+
assert len(e.value.problems) >= 5
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_resolve_emits_the_one_vocabulary(system):
|
|
45
|
+
slug, tok = system
|
|
46
|
+
for mode in T.modes(tok):
|
|
47
|
+
v = T.resolve(tok, mode)
|
|
48
|
+
assert v["system"] == slug and v["mode"] == mode
|
|
49
|
+
for key in ("bg", "surface", "text", "accent", "muted", "muted_solid", "divider", "divider_solid",
|
|
50
|
+
"selection", "selection_solid", "disabled_text", "font_heading", "font_body",
|
|
51
|
+
"font_mono", "font_ui", "fs_body", "fs_mono", "fs_ui", "measure", "line_height",
|
|
52
|
+
"radius_md", "space_2", "fs_h1", "fw_h1", "icon_stroke"):
|
|
53
|
+
assert key in v, key
|
|
54
|
+
for role in T.STATE_ROLES:
|
|
55
|
+
assert v[role].startswith("#"), role
|
|
56
|
+
assert v["dim"].startswith("#") and v["dim"] == v["muted_solid"]
|
|
57
|
+
# a ramp reference resolved to its step
|
|
58
|
+
assert not v["accent_pressed"].startswith("@")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_resolve_passes_system_keys_through():
|
|
62
|
+
tok = T.load(systems.tokens_path("netrunner"))
|
|
63
|
+
v = T.resolve(tok, "blue")
|
|
64
|
+
assert v["fill"] == tok["modes"]["blue"]["fill"]
|
|
65
|
+
assert v["fill_hover"].startswith("rgba(")
|
|
66
|
+
assert v["fs_display"] == "30"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_mode_for_scheme_follows_only_declared_maps():
|
|
70
|
+
classical = T.load(systems.tokens_path("classical"))
|
|
71
|
+
netrunner = T.load(systems.tokens_path("netrunner"))
|
|
72
|
+
assert T.mode_for_scheme(classical, "dark") == "dark"
|
|
73
|
+
assert T.mode_for_scheme(netrunner, "dark") is None
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_color_helpers():
|
|
77
|
+
assert T.rgba("#ff0000", .5) == "rgba(255, 0, 0, 128)"
|
|
78
|
+
assert T.mix("#000000", "#ffffff", .5) == "#808080"
|
|
79
|
+
assert T.to_hex((300, -5, 12.6)) == "#ff000d"
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_to_css_projects_every_mode(system):
|
|
83
|
+
slug, tok = system
|
|
84
|
+
css = T.to_css(tok)
|
|
85
|
+
first = T.modes(tok)[0]
|
|
86
|
+
assert ":root {" in css
|
|
87
|
+
for mode in T.modes(tok)[1:]:
|
|
88
|
+
assert f':root[data-mode="{mode}"]' in css
|
|
89
|
+
assert "--neutral-900:" in css and "--fs-h1:" in css and "--font-body:" in css
|
|
90
|
+
assert f"--bg: {T.resolve(tok, first)['bg']};" in css
|
|
91
|
+
assert "--danger:" in css and "--dim:" in css
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def test_load_round_trips_json(tmp_path):
|
|
95
|
+
tok = T.load(systems.tokens_path("classical"))
|
|
96
|
+
p = tmp_path / "tokens.json"
|
|
97
|
+
p.write_text(json.dumps(tok))
|
|
98
|
+
assert T.load(p) == tok
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def test_systems_locate_by_slug_dir_and_path(tmp_path):
|
|
102
|
+
root = systems.locate("classical")
|
|
103
|
+
assert (root / "tokens.json").is_file()
|
|
104
|
+
assert systems.locate(root) == root
|
|
105
|
+
assert systems.locate(root / "tokens.json") == root
|
|
106
|
+
with pytest.raises(KeyError) as e:
|
|
107
|
+
systems.locate("no-such-system")
|
|
108
|
+
assert "classical" in str(e.value)
|
|
109
|
+
assert systems.available() == ["classical", "netrunner"]
|