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.
- scshafe_qt-0.1.2/.gitignore +7 -0
- scshafe_qt-0.1.2/CHANGELOG.md +92 -0
- scshafe_qt-0.1.2/LICENSE +21 -0
- scshafe_qt-0.1.2/PKG-INFO +334 -0
- scshafe_qt-0.1.2/README.md +305 -0
- scshafe_qt-0.1.2/examples/gallery.py +96 -0
- scshafe_qt-0.1.2/examples/gallery.qml +307 -0
- scshafe_qt-0.1.2/pyproject.toml +80 -0
- scshafe_qt-0.1.2/src/scshafe_qt/__init__.py +49 -0
- scshafe_qt-0.1.2/src/scshafe_qt/qml/Scshafe/Ui/SuiAppShell.qml +175 -0
- scshafe_qt-0.1.2/src/scshafe_qt/qml/Scshafe/Ui/SuiBadge.qml +48 -0
- scshafe_qt-0.1.2/src/scshafe_qt/qml/Scshafe/Ui/SuiBanner.qml +108 -0
- scshafe_qt-0.1.2/src/scshafe_qt/qml/Scshafe/Ui/SuiButton.qml +93 -0
- scshafe_qt-0.1.2/src/scshafe_qt/qml/Scshafe/Ui/SuiChip.qml +68 -0
- scshafe_qt-0.1.2/src/scshafe_qt/qml/Scshafe/Ui/SuiDialog.qml +160 -0
- scshafe_qt-0.1.2/src/scshafe_qt/qml/Scshafe/Ui/SuiEmptyState.qml +82 -0
- scshafe_qt-0.1.2/src/scshafe_qt/qml/Scshafe/Ui/SuiFocusRing.qml +28 -0
- scshafe_qt-0.1.2/src/scshafe_qt/qml/Scshafe/Ui/SuiIcon.qml +96 -0
- scshafe_qt-0.1.2/src/scshafe_qt/qml/Scshafe/Ui/SuiIconButton.qml +76 -0
- scshafe_qt-0.1.2/src/scshafe_qt/qml/Scshafe/Ui/SuiKbd.qml +47 -0
- scshafe_qt-0.1.2/src/scshafe_qt/qml/Scshafe/Ui/SuiList.qml +107 -0
- scshafe_qt-0.1.2/src/scshafe_qt/qml/Scshafe/Ui/SuiListRow.qml +150 -0
- scshafe_qt-0.1.2/src/scshafe_qt/qml/Scshafe/Ui/SuiSearchField.qml +84 -0
- scshafe_qt-0.1.2/src/scshafe_qt/qml/Scshafe/Ui/SuiSheet.qml +41 -0
- scshafe_qt-0.1.2/src/scshafe_qt/qml/Scshafe/Ui/SuiShortcutOverlay.qml +158 -0
- scshafe_qt-0.1.2/src/scshafe_qt/qml/Scshafe/Ui/SuiSidebarItem.qml +102 -0
- scshafe_qt-0.1.2/src/scshafe_qt/qml/Scshafe/Ui/SuiSidebarList.qml +150 -0
- scshafe_qt-0.1.2/src/scshafe_qt/qml/Scshafe/Ui/SuiSplitHandle.qml +74 -0
- scshafe_qt-0.1.2/src/scshafe_qt/qml/Scshafe/Ui/SuiTextField.qml +69 -0
- scshafe_qt-0.1.2/src/scshafe_qt/qml/Scshafe/Ui/SuiTheme.qml +414 -0
- scshafe_qt-0.1.2/src/scshafe_qt/qml/Scshafe/Ui/SuiToast.qml +121 -0
- scshafe_qt-0.1.2/src/scshafe_qt/qml/Scshafe/Ui/SuiToastHost.qml +116 -0
- scshafe_qt-0.1.2/src/scshafe_qt/qml/Scshafe/Ui/SuiTrail.qml +175 -0
- scshafe_qt-0.1.2/src/scshafe_qt/qml/Scshafe/Ui/qmldir +25 -0
- scshafe_qt-0.1.2/tests/conftest.py +166 -0
- scshafe_qt-0.1.2/tests/qml/tst_components.qml +199 -0
- scshafe_qt-0.1.2/tests/qml/tst_sui.qml +88 -0
- scshafe_qt-0.1.2/tests/qml_runner.py +34 -0
- scshafe_qt-0.1.2/tests/test_button.py +119 -0
- scshafe_qt-0.1.2/tests/test_components.py +306 -0
- scshafe_qt-0.1.2/tests/test_dialogs.py +297 -0
- scshafe_qt-0.1.2/tests/test_feedback.py +159 -0
- scshafe_qt-0.1.2/tests/test_gallery.py +62 -0
- scshafe_qt-0.1.2/tests/test_lists.py +228 -0
- scshafe_qt-0.1.2/tests/test_module.py +46 -0
- scshafe_qt-0.1.2/tests/test_plain_text.py +126 -0
- scshafe_qt-0.1.2/tests/test_qml.py +28 -0
- scshafe_qt-0.1.2/tests/test_rules.py +187 -0
- scshafe_qt-0.1.2/tests/test_shell.py +152 -0
- scshafe_qt-0.1.2/tests/test_theme.py +85 -0
- scshafe_qt-0.1.2/tests/test_tokens.py +348 -0
- scshafe_qt-0.1.2/tokens/sui-tokens.json +572 -0
- scshafe_qt-0.1.2/tools/check_dist.py +196 -0
- scshafe_qt-0.1.2/tools/gen_tokens.py +678 -0
|
@@ -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.
|
scshafe_qt-0.1.2/LICENSE
ADDED
|
@@ -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).
|