scshafe-qt 0.1.2__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.
Files changed (54) hide show
  1. scshafe_qt-0.1.2/.gitignore +7 -0
  2. scshafe_qt-0.1.2/CHANGELOG.md +92 -0
  3. scshafe_qt-0.1.2/LICENSE +21 -0
  4. scshafe_qt-0.1.2/PKG-INFO +334 -0
  5. scshafe_qt-0.1.2/README.md +305 -0
  6. scshafe_qt-0.1.2/examples/gallery.py +96 -0
  7. scshafe_qt-0.1.2/examples/gallery.qml +307 -0
  8. scshafe_qt-0.1.2/pyproject.toml +80 -0
  9. scshafe_qt-0.1.2/src/scshafe_qt/__init__.py +49 -0
  10. scshafe_qt-0.1.2/src/scshafe_qt/qml/Scshafe/Ui/SuiAppShell.qml +175 -0
  11. scshafe_qt-0.1.2/src/scshafe_qt/qml/Scshafe/Ui/SuiBadge.qml +48 -0
  12. scshafe_qt-0.1.2/src/scshafe_qt/qml/Scshafe/Ui/SuiBanner.qml +108 -0
  13. scshafe_qt-0.1.2/src/scshafe_qt/qml/Scshafe/Ui/SuiButton.qml +93 -0
  14. scshafe_qt-0.1.2/src/scshafe_qt/qml/Scshafe/Ui/SuiChip.qml +68 -0
  15. scshafe_qt-0.1.2/src/scshafe_qt/qml/Scshafe/Ui/SuiDialog.qml +160 -0
  16. scshafe_qt-0.1.2/src/scshafe_qt/qml/Scshafe/Ui/SuiEmptyState.qml +82 -0
  17. scshafe_qt-0.1.2/src/scshafe_qt/qml/Scshafe/Ui/SuiFocusRing.qml +28 -0
  18. scshafe_qt-0.1.2/src/scshafe_qt/qml/Scshafe/Ui/SuiIcon.qml +96 -0
  19. scshafe_qt-0.1.2/src/scshafe_qt/qml/Scshafe/Ui/SuiIconButton.qml +76 -0
  20. scshafe_qt-0.1.2/src/scshafe_qt/qml/Scshafe/Ui/SuiKbd.qml +47 -0
  21. scshafe_qt-0.1.2/src/scshafe_qt/qml/Scshafe/Ui/SuiList.qml +107 -0
  22. scshafe_qt-0.1.2/src/scshafe_qt/qml/Scshafe/Ui/SuiListRow.qml +150 -0
  23. scshafe_qt-0.1.2/src/scshafe_qt/qml/Scshafe/Ui/SuiSearchField.qml +84 -0
  24. scshafe_qt-0.1.2/src/scshafe_qt/qml/Scshafe/Ui/SuiSheet.qml +41 -0
  25. scshafe_qt-0.1.2/src/scshafe_qt/qml/Scshafe/Ui/SuiShortcutOverlay.qml +158 -0
  26. scshafe_qt-0.1.2/src/scshafe_qt/qml/Scshafe/Ui/SuiSidebarItem.qml +102 -0
  27. scshafe_qt-0.1.2/src/scshafe_qt/qml/Scshafe/Ui/SuiSidebarList.qml +150 -0
  28. scshafe_qt-0.1.2/src/scshafe_qt/qml/Scshafe/Ui/SuiSplitHandle.qml +74 -0
  29. scshafe_qt-0.1.2/src/scshafe_qt/qml/Scshafe/Ui/SuiTextField.qml +69 -0
  30. scshafe_qt-0.1.2/src/scshafe_qt/qml/Scshafe/Ui/SuiTheme.qml +414 -0
  31. scshafe_qt-0.1.2/src/scshafe_qt/qml/Scshafe/Ui/SuiToast.qml +121 -0
  32. scshafe_qt-0.1.2/src/scshafe_qt/qml/Scshafe/Ui/SuiToastHost.qml +116 -0
  33. scshafe_qt-0.1.2/src/scshafe_qt/qml/Scshafe/Ui/SuiTrail.qml +175 -0
  34. scshafe_qt-0.1.2/src/scshafe_qt/qml/Scshafe/Ui/qmldir +25 -0
  35. scshafe_qt-0.1.2/tests/conftest.py +166 -0
  36. scshafe_qt-0.1.2/tests/qml/tst_components.qml +199 -0
  37. scshafe_qt-0.1.2/tests/qml/tst_sui.qml +88 -0
  38. scshafe_qt-0.1.2/tests/qml_runner.py +34 -0
  39. scshafe_qt-0.1.2/tests/test_button.py +119 -0
  40. scshafe_qt-0.1.2/tests/test_components.py +306 -0
  41. scshafe_qt-0.1.2/tests/test_dialogs.py +297 -0
  42. scshafe_qt-0.1.2/tests/test_feedback.py +159 -0
  43. scshafe_qt-0.1.2/tests/test_gallery.py +62 -0
  44. scshafe_qt-0.1.2/tests/test_lists.py +228 -0
  45. scshafe_qt-0.1.2/tests/test_module.py +46 -0
  46. scshafe_qt-0.1.2/tests/test_plain_text.py +126 -0
  47. scshafe_qt-0.1.2/tests/test_qml.py +28 -0
  48. scshafe_qt-0.1.2/tests/test_rules.py +187 -0
  49. scshafe_qt-0.1.2/tests/test_shell.py +152 -0
  50. scshafe_qt-0.1.2/tests/test_theme.py +85 -0
  51. scshafe_qt-0.1.2/tests/test_tokens.py +348 -0
  52. scshafe_qt-0.1.2/tokens/sui-tokens.json +572 -0
  53. scshafe_qt-0.1.2/tools/check_dist.py +196 -0
  54. scshafe_qt-0.1.2/tools/gen_tokens.py +678 -0
@@ -0,0 +1,7 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
4
+ dist/
5
+ build/
6
+ .pytest_cache/
7
+ *.egg-info/
@@ -0,0 +1,92 @@
1
+ # Changelog
2
+
3
+ All notable changes to `scshafe-qt` are recorded here. Versions follow
4
+ [SemVer](https://semver.org/). A release is the annotated tag `v<x.y.z>` on a
5
+ commit on `main` whose `pyproject.toml` version is `<x.y.z>`; released versions
6
+ are never deleted, replaced or reused.
7
+
8
+ ## Unreleased
9
+
10
+ ## 0.1.2 — 2026-10-05
11
+
12
+ Published to PyPI; docs. No library change (the wheel's code and QML module
13
+ are those of 0.1.1).
14
+
15
+ - **On PyPI:** `pip install scshafe-qt` / `uv add scshafe-qt`. `publish.yml`
16
+ gains a `pypi` job: after the GitHub Release is published it uploads the
17
+ build job's wheel and sdist (a run artifact; the Release carries the same
18
+ files) by trusted publishing (OIDC, environment `pypi`, `v*` tags only,
19
+ PEP 740 attestations), then checks that PyPI serves the verified digests.
20
+ - Package metadata for PyPI: dropped the `Private :: Do Not Upload`
21
+ classifier (PyPI refuses it), the misleading `Framework :: Pytest` and the
22
+ `License ::` classifier (the license is the PEP 639 expression `MIT`); added
23
+ keywords, status, audience, OS and topic classifiers and project URLs
24
+ (homepage, docs, issues, releases).
25
+ - README: install from PyPI, or from a Release URL as a fallback (the
26
+ repository is public: Release downloads need no token); the CI token-check
27
+ note no longer calls `@scshafe/ui` private.
28
+ ## 0.1.1 — 2026-10-02
29
+
30
+ Security release.
31
+
32
+ - **Caller text is plain text.** Every `Text` in `Scshafe.Ui` sets
33
+ `textFormat: Text.PlainText` (25 elements, 14 components). With Qt's default
34
+ `AutoText`, a caller string that looked like HTML rendered as rich text and
35
+ fetched remote images: a mail subject with `<img src=…>` made Qt request it,
36
+ a tracking beacon (found by mailroom-desktop's review). No component needs
37
+ rich text, so there is no opt-in. `tests/test_plain_text.py` proves it with
38
+ a counting HTTP server (0.1.0: fetched; 0.1.1: zero requests) and fails any
39
+ new `Text` without the line.
40
+ - Token snapshot from `@scshafe/ui` 0.4.1: the registry is unchanged (same
41
+ sha256); only the recorded source version moves.
42
+ ## 0.1.0 — 2026-10-01
43
+
44
+ First release.
45
+
46
+ - Releases: `.github/workflows/publish.yml` (the library standard's Python
47
+ variant): an annotated `v<x.y.z>` tag on `main` matching the version; tests,
48
+ build and install-and-load smoke on Linux and macOS; a reproducible rebuild
49
+ must match; a draft GitHub Release with the wheel, sdist and `SHA256SUMS`,
50
+ published only after the assets downloaded back pass the payload check and
51
+ smoke. `tools/check_dist.py --dist DIR`. CI runs macOS by hand and weekly
52
+ (tags go through `publish.yml`).
53
+ - Tests ignore one headless-only Qt warning: on macOS the offscreen platform's
54
+ theme font "Sans Serif" doesn't exist; any other warning still fails.
55
+ - Qt 6.11: depend on `PySide6-Essentials>=6.11.2,<6.12` (was `>=6.8.3,<6.9`);
56
+ open-source Qt patches track the current minor, so the library follows it
57
+ (6.12 when PySide6 6.12 ships). Python 3.14 (`.python-version`,
58
+ `requires-python >=3.14,<3.15`); the 6.11 wheels are abi3 (CPython 3.10+).
59
+ - Q1 components (QtQuick.Templates, themed from `SuiTheme`, light and dark,
60
+ keyboard-focusable with a keyboard-only focus ring, accessible roles and
61
+ names, animations on `SuiTheme.duration`): `SuiAppShell` (resizable,
62
+ keyboard-operable splitters; collapsible inspector and sidebar),
63
+ `SuiSidebarList` / `SuiSidebarItem` (counts, ok/held/failing dots, sections),
64
+ `SuiList` / `SuiListRow` (single selection; `j`/`k`/arrow hooks as signals),
65
+ `SuiChip`, `SuiBadge`, `SuiIconButton` (required `label`), `SuiTextField`,
66
+ `SuiSearchField` (`/` focus hook, Escape clears), `SuiEmptyState`, `SuiBanner`,
67
+ `SuiToast` / `SuiToastHost`, `SuiDialog` / `SuiSheet` (focus trap, Escape,
68
+ focus return), `SuiTrail`, `SuiShortcutOverlay` (`?`), `SuiIcon` (built-in
69
+ vector icons), `SuiKbd`; internal `SuiFocusRing`, `SuiSplitHandle`.
70
+ - `SuiTheme` NATIVE-ONLY tokens (generator section, not in the registry):
71
+ eight bucket tones `bucket1`…`bucket8` (contrast-checked: text >= 4.5:1 on
72
+ the chip over every surface and overlay, base >= 3:1, in both themes), tone
73
+ helpers, control metrics and layout defaults, `monoFamily` per platform.
74
+ - `examples/gallery.py`: every component in one window; `--screenshot DIR`.
75
+ - Tests: per-component render (both themes), accessible role and name,
76
+ keyboard focus ring (pixel), pointer focus without ring, focus order, list
77
+ and sidebar keyboard navigation, shell resizing, dialog focus trap and
78
+ return, toasts, no colour literals, every animation on `SuiTheme.duration`
79
+ (static and at runtime), token parity and contrast, gallery screenshots in
80
+ both themes; Qt warnings fail tests. `tools/check_dist.py` checks every
81
+ module file is in the wheel and listed in `qmldir`, and the smoke builds
82
+ every public component from the installed wheel.
83
+ - Q0 scaffold: package `scshafe_qt` (hatchling, uv, Python 3.13) depending on
84
+ `PySide6-Essentials>=6.8.3,<6.9` (Qt 6.8 LTS), with the QML module
85
+ `Scshafe.Ui` and `scshafe_qt.register(engine)` / `qml_import_path()`.
86
+ - `SuiTheme` singleton generated from the `@scshafe/ui` 0.3.1 token registry
87
+ (62 tokens, light and dark): `mode` system/light/dark, `reducedMotion`, plus
88
+ native focus-ring and type-scale values; `tools/gen_tokens.py --check`.
89
+ - `SuiButton`: themed, keyboard-focusable, keyboard-only focus ring,
90
+ accessible role and name.
91
+ - Tests (pytest-qt and QtQuickTest, offscreen), CI on Linux (every push) and
92
+ macOS (tags, dispatch, weekly), distribution check.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 scshafe-qt contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,334 @@
1
+ Metadata-Version: 2.5
2
+ Name: scshafe-qt
3
+ Version: 0.1.2
4
+ Summary: The SCSHAFE native component library: the Scshafe.Ui QML module (Qt Quick) for PySide6 apps, themed from the @scshafe/ui token registry.
5
+ Project-URL: Homepage, https://github.com/scshafe/scshafe-qt
6
+ Project-URL: Documentation, https://github.com/scshafe/scshafe-qt#readme
7
+ Project-URL: Repository, https://github.com/scshafe/scshafe-qt
8
+ Project-URL: Issues, https://github.com/scshafe/scshafe-qt/issues
9
+ Project-URL: Changelog, https://github.com/scshafe/scshafe-qt/blob/main/CHANGELOG.md
10
+ Project-URL: Releases, https://github.com/scshafe/scshafe-qt/releases
11
+ Author: scshafe-qt contributors
12
+ License-Expression: MIT
13
+ License-File: LICENSE
14
+ Keywords: components,design-tokens,pyside6,qml,qt,qt-quick,ui
15
+ Classifier: Development Status :: 3 - Alpha
16
+ Classifier: Environment :: MacOS X
17
+ Classifier: Environment :: X11 Applications :: Qt
18
+ Classifier: Intended Audience :: Developers
19
+ Classifier: Operating System :: MacOS
20
+ Classifier: Operating System :: POSIX :: Linux
21
+ Classifier: Programming Language :: Python :: 3
22
+ Classifier: Programming Language :: Python :: 3 :: Only
23
+ Classifier: Programming Language :: Python :: 3.14
24
+ Classifier: Topic :: Software Development :: Libraries
25
+ Classifier: Topic :: Software Development :: User Interfaces
26
+ Requires-Python: <3.15,>=3.14
27
+ Requires-Dist: pyside6-essentials<6.12,>=6.11.2
28
+ Description-Content-Type: text/markdown
29
+
30
+ # scshafe-qt
31
+
32
+ The SCSHAFE native component library: the QML module **`Scshafe.Ui`** (Qt Quick) for
33
+ Python + PySide6 desktop apps, themed from the same token registry as the web library
34
+ `@scshafe/ui`, so web and native share one source of truth for colour, spacing, radii,
35
+ type and motion.
36
+
37
+ - Distribution `scshafe-qt`, import `scshafe_qt`, QML module `Scshafe.Ui`.
38
+ - Qt **6.11** through `PySide6-Essentials` 6.11.x (6.11.2). Open-source Qt patch
39
+ releases track the current minor (6.8 LTS patches for open source ended at
40
+ 6.8.3), so the library follows the current minor; 6.12 comes when PySide6 6.12
41
+ ships.
42
+ - Python 3.14 (the 6.11 wheels are abi3 for CPython 3.10+ and declare
43
+ `requires-python <3.15`).
44
+ - Status: **0.1.2**: `SuiTheme` (tokens) and the v0.1 component set below.
45
+ - **Untrusted text is safe to pass:** every component renders caller strings as plain text
46
+ (`textFormat: Text.PlainText`); markup is shown literally and never fetches anything.
47
+ 0.1.0 rendered HTML-looking strings as rich text (fixed in 0.1.1; upgrade).
48
+
49
+ ## Install (consumers)
50
+
51
+ From [PyPI](https://pypi.org/project/scshafe-qt/) (from 0.1.2):
52
+
53
+ ```sh
54
+ uv add scshafe-qt # or: pip install scshafe-qt
55
+ ```
56
+
57
+ Every release is also a GitHub Release of `scshafe/scshafe-qt` with the same wheel and
58
+ sdist, their sha256 in the notes and a `SHA256SUMS` file. `publish.yml` uploads the
59
+ very files it attached to the Release (PyPI and the Release are byte-identical), with
60
+ PEP 740 attestations from trusted publishing. As a fallback, install straight from a
61
+ Release; the repository is public, so the download URLs need no token:
62
+
63
+ ```sh
64
+ pip install https://github.com/scshafe/scshafe-qt/releases/download/v0.1.2/scshafe_qt-0.1.2-py3-none-any.whl
65
+ # or, in a uv project, a pinned URL source (uv.lock records its sha256):
66
+ uv add "scshafe-qt @ https://github.com/scshafe/scshafe-qt/releases/download/v0.1.2/scshafe_qt-0.1.2-py3-none-any.whl"
67
+ ```
68
+
69
+ To check the files yourself: `gh release download v0.1.2 -R scshafe/scshafe-qt -p '*.whl' -p SHA256SUMS`,
70
+ then `sha256sum -c --ignore-missing SHA256SUMS`.
71
+
72
+ The library accepts the Qt minor it is tested on (`PySide6-Essentials>=6.11.2,<6.12`);
73
+ the app's own `uv.lock` pins one exact PySide6.
74
+
75
+ ## Usage
76
+
77
+ ```python
78
+ import sys
79
+ from PySide6.QtGui import QGuiApplication
80
+ from PySide6.QtQml import QQmlApplicationEngine
81
+ import scshafe_qt
82
+
83
+ app = QGuiApplication(sys.argv)
84
+ engine = QQmlApplicationEngine()
85
+ scshafe_qt.register(engine) # adds scshafe_qt.qml_import_path() once
86
+ engine.load("main.qml")
87
+ sys.exit(app.exec())
88
+ ```
89
+
90
+ ```qml
91
+ import QtQuick
92
+ import Scshafe.Ui
93
+
94
+ Window {
95
+ visible: true
96
+ color: SuiTheme.bg
97
+ SuiButton { text: "Sort"; variant: "primary"; onClicked: console.log("sorted") }
98
+ }
99
+ ```
100
+
101
+ Imports are versionless (`import Scshafe.Ui`).
102
+
103
+ ## Components
104
+
105
+ | Component | What it is | Keyboard / accessibility |
106
+ | --- | --- | --- |
107
+ | `SuiAppShell` | sidebar \| content \| inspector frame; `sidebar`, `content`, `inspector` slots; `inspectorOpen` / `sidebarOpen` collapse a pane | splitters are Tab stops (role Separator): ←/→ resize by `resizeStep` (Shift ×4), Home/End min/max, Enter collapses; drag resizes; panes are named Panes |
108
+ | `SuiSidebarList` + `SuiSidebarItem` | navigation list: icon, label, count badge, health dot (`status`: `ok` / `held` / `failing` / `none`), section headers (`section` role) | one Tab stop; ↑/↓ Home/End PgUp/PgDn move (skip disabled), Enter/Return/Space activate (`activated(index)`, `selectedIndex`); status and count in the accessible description |
109
+ | `SuiList` + `SuiListRow` | single-selection `ListView` (`currentIndex`) of two-line rows (`title`, `subtitle`, `meta`, `unread`; children go to a trailing slot) | one Tab stop; ↓/`j`, ↑/`k` emit `nextRequested()` / `previousRequested()` and move (unless `autoNavigate: false`); Enter / double-click emit `activated(index)`; `selectNext()` etc. for app shortcuts |
110
+ | `SuiChip`, `SuiBadge` | pills: `tone` `neutral` / `info` / `ok` / `warn` / `danger` or a bucket `bucket1`…`bucket8` (or 1–8); badge shows `count` (99+) | StaticText named by the text |
111
+ | `SuiButton`, `SuiIconButton` | buttons; the icon button needs `label` (its accessible name; a `required` property) and `icon.name` (built-in) or `icon.source`; `checkable` toggles | Space/Enter; role Button (CheckBox when checkable) |
112
+ | `SuiTextField`, `SuiSearchField` | inputs; search has an icon, a clear button and a `/` hook | `/` anywhere (not while typing in another field) fires `focusRequested()` then focuses and selects; Escape clears (`cleared()`), then propagates |
113
+ | `SuiEmptyState` | icon, title, description, action slot | Grouping named by the title |
114
+ | `SuiBanner` | inline status (`tone` info / ok / warn / danger), `dismissible` | warn/danger are AlertMessages; the dismiss button is a Tab stop |
115
+ | `SuiToast`, `SuiToastHost` | `host.show(text, { title, tone, timeout, actionText, onAction })`, `dismiss(id)`, `clear()`; stacks bottom-right, at most `maxToasts` | AlertMessage; announced with `Accessible.announce` (assertive for danger); the timeout pauses on hover / focus; Escape dismisses |
116
+ | `SuiDialog`, `SuiSheet` | modal dialog (`title`, `description`, content, `actions`) and a sheet sliding from an `edge` | focus moves in on open, Tab is trapped inside, Escape rejects, focus returns to the opener (with its ring if it had keyboard focus); role Dialog named by the title |
117
+ | `SuiTrail` | vertical steps: `title`, `outcome` (+ `outcomeTone`), `detail`, `via` tag, `warning` marker | List of ListItems ("2. Classifier: Receipts", description = detail, via, warning) |
118
+ | `SuiShortcutOverlay` | the `?` overlay of the app's `shortcuts` (`keys`, `description`, `group`, `separator`) | `?` toggles (not while typing); a SuiDialog |
119
+ | `SuiIcon`, `SuiKbd` | built-in vector icons (`SuiIcon.names`) or a tinted image; a keyboard-key badge | icons are decorative unless given a `label` |
120
+
121
+ ```qml
122
+ import QtQuick
123
+ import Scshafe.Ui
124
+
125
+ Window {
126
+ width: 1100; height: 700; visible: true; color: SuiTheme.bg
127
+ Shortcut { sequence: "j"; onActivated: inbox.selectNext() } // from anywhere
128
+ SuiAppShell {
129
+ anchors.fill: parent
130
+ sidebar: SuiSidebarList {
131
+ label: "Mailboxes"; selectedIndex: 0
132
+ model: [ { section: "Buckets", text: "Receipts", iconName: "tag", count: 3, status: "ok" } ]
133
+ }
134
+ content: SuiList {
135
+ id: inbox; label: "Messages"; model: messages
136
+ delegate: SuiListRow {
137
+ width: ListView.view.width
138
+ title: model.sender; subtitle: model.subject; meta: model.time; unread: model.unread
139
+ SuiChip { text: model.bucketName; tone: model.bucket; dot: true } // trailing slot
140
+ }
141
+ onActivated: (index) => sheet.open()
142
+ }
143
+ inspector: SuiTrail { steps: [ { title: "Classifier", outcome: "Receipts", outcomeTone: 1, via: "model" } ] }
144
+ }
145
+ SuiToastHost { id: toasts; anchors.fill: parent; z: 100 }
146
+ }
147
+ ```
148
+
149
+ Every interactive component is keyboard-focusable and draws its focus ring for
150
+ keyboard focus only (text fields for any focus, as browsers do). Lists are one
151
+ Tab stop with roving focus on the current row, which holds active focus (so a
152
+ screen reader announces it); the ring returns after a pointer click as soon as
153
+ an arrow key is used (the `:focus-visible` heuristic).
154
+
155
+ ### Gallery
156
+
157
+ `examples/gallery.py` shows every component in one window with made-up data:
158
+
159
+ ```sh
160
+ uv run python examples/gallery.py # follows the OS theme
161
+ uv run python examples/gallery.py --theme dark --reduced-motion
162
+ uv run python examples/gallery.py --screenshot DIR # PNGs of four views x two themes
163
+ ```
164
+
165
+ In the window: Tab / Shift+Tab, `j` / `k`, `1`–`8` (a toast), `/`, `?`.
166
+
167
+ ### Theme and motion
168
+
169
+ `SuiTheme` is a singleton with every registry token as a property (`--sui-text-strong`
170
+ is `SuiTheme.textStrong`; `SuiTheme.registry` maps CSS names to property names).
171
+
172
+ - `SuiTheme.mode`: `"system"` (default) follows `Qt.styleHints.colorScheme`; `"light"`
173
+ and `"dark"` pin a theme. `SuiTheme.dark` / `SuiTheme.themeName` report the result.
174
+ - `SuiTheme.reducedMotion`: when `true`, `SuiTheme.duration` (every transition) is 0.
175
+ Qt (through 6.11) exposes **no** OS reduced-motion preference (QStyleHints has none;
176
+ `QAccessibilityHints`, 6.10+, only carries `contrastPreference`), so the application sets it,
177
+ e.g. from GNOME's `org.gnome.desktop.interface enable-animations` or macOS's
178
+ "Reduce motion" setting.
179
+ - Units: lengths are logical pixels (CSS px), durations milliseconds; shadows are
180
+ `{ offsetX, offsetY, blur, spread, color }` for `MultiEffect`.
181
+ - Native-only tokens (not in the registry) live in the clearly marked NATIVE-ONLY
182
+ section of `tools/gen_tokens.py` and are generated into `SuiTheme.qml`: focus ring,
183
+ control metrics and layout defaults, type scale, the bucket palette, tone helpers
184
+ (`toneBase`, `toneText`, `toneFill`, `toneBorder`, `statusColor`), `monoFamily` and
185
+ `alpha(color, amount)`.
186
+
187
+ ### Tones and the bucket palette
188
+
189
+ Status tones reuse the registry's tone tokens: `info` blue, `ok` green, `warn`
190
+ yellow, `danger` red; `neutral` is `--sui-text` on `--sui-tint`. A chip's fill is
191
+ its tone at `toneTint` (12 %, the web library's `TONE_TINT`), its border at 45 %.
192
+
193
+ `bucket1`…`bucket8` are native-only categorical tones for user-defined groups,
194
+ chosen in OKLCH about 45° apart (blue, teal, green, olive, amber, rust, rose,
195
+ violet). `tests/test_tokens.py` checks, in both themes, that each bucket's text
196
+ reaches 4.5:1 on its chip fill over every surface and every overlay (selection,
197
+ hover, tint), that each base (dots, swatches) reaches 3:1 on every surface, and that
198
+ the bases stay pairwise distinct (OKLab distance ≥ 0.07). Lowest ratios today:
199
+
200
+ | | blue | teal | green | olive | amber | rust | rose | violet |
201
+ | --- | --- | --- | --- | --- | --- | --- | --- | --- |
202
+ | light text on chip | 5.08 | 4.99 | 4.98 | 5.01 | 5.04 | 4.98 | 4.96 | 4.96 |
203
+ | dark text on chip | 6.01 | 6.03 | 6.08 | 6.01 | 6.01 | 6.02 | 6.12 | 6.02 |
204
+ | light base on surfaces | 3.69 | 3.73 | 3.73 | 3.68 | 3.75 | 3.76 | 3.82 | 3.77 |
205
+ | dark base on surfaces | 7.20 | 7.62 | 7.66 | 7.37 | 7.10 | 6.87 | 6.75 | 6.90 |
206
+
207
+ ### Monospace
208
+
209
+ `--sui-mono` is a CSS stack; a Qt font takes one family, so `SuiTheme.monoFamily`
210
+ is the first installed candidate for the platform (assign it to override):
211
+
212
+ - Linux: `monospace`, fontconfig's alias for the user's configured monospace face
213
+ (DejaVu Sans Mono, Noto Sans Mono, Liberation Mono or Ubuntu Mono on common
214
+ distributions);
215
+ - macOS: SF Mono (when installed), else Menlo (always present), Monaco;
216
+ - Windows: Cascadia Mono, Consolas, Courier New;
217
+ - fallback: `monospace`.
218
+
219
+ ### Accessibility contract
220
+
221
+ Every interactive component is keyboard-focusable, shows a focus ring
222
+ (`SuiTheme.focusRing`, outside the control, or inset on list rows) for keyboard
223
+ focus only (Qt's `visualFocus`, the native `:focus-visible`), sets
224
+ `Accessible.role` and `Accessible.name`, and animates only on `SuiTheme.duration`.
225
+ Components carry no colour literals (`tests/test_rules.py` enforces it, as
226
+ `@scshafe/ui` does for its stylesheets) and every animation runs on
227
+ `SuiTheme.duration`, so `reducedMotion` stops them all (checked statically and at
228
+ runtime over the gallery).
229
+
230
+ ### Qt version notes
231
+
232
+ The QML targets Qt 6.11 and uses no 6.9–6.11-only QML API: the suite also passes
233
+ on PySide6 6.8.3 except for the dialog's accessible name (below). Worth knowing:
234
+
235
+ - `SuiIcon` tints image icons with `IconImage` from `QtQuick.Controls.impl`, the
236
+ module Qt's own styles use; it is not a public API with compatibility promises,
237
+ so a Qt upgrade re-checks it (the tests load it).
238
+ - `SuiToastHost` announces toasts with `Accessible.announce()` (Qt 6.8+), guarded.
239
+ - A dialog's accessible name comes from Qt: on 6.11 `T.Dialog` names its popup by
240
+ `title` once accessibility is active (an assistive technology is running); Qt
241
+ 6.8.3 leaves it unnamed.
242
+ - Qt (through 6.11) reports no OS reduced-motion preference; the app sets
243
+ `SuiTheme.reducedMotion`.
244
+
245
+ ## Token pipeline
246
+
247
+ ```
248
+ @scshafe/ui ./tokens export (lib/tokens.js), cross-checked with src/tokens.ts
249
+ │ tools/gen_tokens.py --refresh (node reads the registry)
250
+ ▼
251
+ tokens/sui-tokens.json committed snapshot: version, file sha256,
252
+ │ source commit, registry hash, tokens
253
+ │ tools/gen_tokens.py
254
+ ▼
255
+ src/scshafe_qt/qml/Scshafe/Ui/SuiTheme.qml committed, header records the source
256
+ ```
257
+
258
+ - `uv run python tools/gen_tokens.py --refresh [--from DIR]` re-reads the registry
259
+ (DIR: a scshafe-ui checkout or an installed `node_modules/@scshafe/ui`; default
260
+ `$SCSHAFE_UI_DIR`, then a sibling `../scshafe-ui`) and rewrites both files. Needs Node.
261
+ - `uv run python tools/gen_tokens.py` regenerates `SuiTheme.qml` from the snapshot.
262
+ - `uv run python tools/gen_tokens.py --check` fails if `SuiTheme.qml` or the qmldir
263
+ `singleton` entry is stale, and, when the registry is reachable, if the snapshot is.
264
+ CI has no scshafe-ui checkout, so there it checks QML against the snapshot and
265
+ says it skipped the registry comparison (`--require-source` makes that a failure).
266
+ - `tests/test_tokens.py` checks every token in both themes against the snapshot
267
+ (and the live registry when reachable).
268
+
269
+ Change a token in scshafe-ui, release it, then `--refresh` here in one commit.
270
+
271
+ ## Development
272
+
273
+ Toolchain: [uv](https://docs.astral.sh/uv/) 0.12.21 (`[tool.uv] required-version`),
274
+ Python 3.14 (`.python-version`; uv uses a matching interpreter or downloads a managed
275
+ CPython 3.14, as CI does), `uv.lock` committed.
276
+
277
+ ```sh
278
+ uv sync --frozen # .venv with PySide6 + pytest-qt
279
+ uv run python tools/gen_tokens.py --check
280
+ uv run pytest # offscreen; includes the QML TestCases
281
+ uv run python tests/qml_runner.py # QML TestCases alone (tests/qml/tst_*.qml)
282
+ SCSHAFE_QT_SCREENSHOTS=DIR uv run pytest tests/test_gallery.py # gallery PNGs into DIR
283
+ uv build && uv run python tools/check_dist.py --smoke
284
+ ```
285
+
286
+ Tests run under `QT_QPA_PLATFORM=offscreen` and `QT_QUICK_BACKEND=software` (set by
287
+ `tests/conftest.py` and `tests/qml_runner.py`). PySide6 wheels ship no
288
+ `qmltestrunner`; `tests/qml_runner.py` uses `PySide6.QtQuickTest`'s
289
+ `QUICK_TEST_MAIN_WITH_SETUP` with `scshafe_qt.register`. Any Qt or QML warning
290
+ fails a test (`qt_log_level_fail = "WARNING"`). The gallery screenshot test writes
291
+ its PNGs to `$SCSHAFE_QT_SCREENSHOTS` (else pytest's temporary directory); they are
292
+ for review and never committed.
293
+
294
+ On Ubuntu the wheels need these system libraries (CI installs them): `libegl1 libgl1
295
+ libxkbcommon0 libfontconfig1 libfreetype6 libx11-6 libglib2.0-0t64 libdbus-1-3` and a
296
+ font (`fonts-dejavu-core`). ICU is bundled.
297
+
298
+ To try a change in an app before a release, install this checkout into the app's
299
+ environment (`uv pip install -e <path>` in the app's venv) and never commit that.
300
+
301
+ ## CI
302
+
303
+ `.github/workflows/ci.yml`, GitHub-hosted runners only, `contents: read`, actions pinned
304
+ by SHA: Linux (`ubuntu-latest`) on every push and pull request; macOS (`macos-latest`,
305
+ Apple Silicon) on `workflow_dispatch` and weekly, to keep macOS minutes low (release
306
+ tags run `publish.yml`, which verifies on both).
307
+ Both run `uv sync --frozen`, the token check, the tests, `uv build` and the
308
+ distribution check (wheel carries the QML module, payload scan, install-and-load smoke).
309
+
310
+ ## Releasing
311
+
312
+ The library standard's Python variant. `pyproject.toml` `version` is the authority: a
313
+ release commit bumps it and adds `## <x.y.z> — <date>` to `CHANGELOG.md`; after CI is
314
+ green on `main` the owning agent runs the dry run
315
+ (`gh workflow run publish.yml -f dry_run=true`, every check on Linux and macOS) and then
316
+ pushes the annotated tag `v<x.y.z>` on that commit. `.github/workflows/publish.yml` is
317
+ the only publisher. It refuses tags not on `main`, not annotated or not matching the
318
+ version, and lock files with non-registry sources; tests, builds and smoke-installs on
319
+ Linux and macOS; rebuilds the tag and requires the same bytes (hatchling builds are
320
+ reproducible); creates a **draft** Release with the wheel, sdist and `SHA256SUMS`;
321
+ downloads those assets back, checks their hashes and runs the payload check and the
322
+ install-and-load smoke on the downloaded wheel; and only then publishes the Release.
323
+ Every Release asset is the build job's own file (passed as a run artifact); the rebuild
324
+ only proves reproducibility. Last, the `pypi` job (environment `pypi`, deployable from
325
+ `v*` tags only; `id-token: write`, no stored token) checks the same files against the
326
+ build digests and the Release's `SHA256SUMS`, uploads them to PyPI by trusted publishing
327
+ (`pypa/gh-action-pypi-publish`, with attestations) and confirms PyPI serves those
328
+ digests. If that job fails, the Release stays; re-run the failed job. The dry run stops
329
+ before the Release and PyPI. Versions are never reused, moved or deleted, here or on
330
+ PyPI (PyPI never accepts a file name twice).
331
+
332
+ ## License
333
+
334
+ MIT, see [LICENSE](https://github.com/scshafe/scshafe-qt/blob/main/LICENSE).