@greendrake/theme 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +92 -0
- package/_index.scss +174 -0
- package/_mixins.scss +138 -0
- package/dashboard-palette.scss +44 -0
- package/dashboard.scss +63 -0
- package/editor-rows.scss +27 -0
- package/form-controls.scss +48 -0
- package/package.json +63 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Eugene Greendrake
|
|
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.
|
package/README.md
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# @greendrake/theme
|
|
2
|
+
|
|
3
|
+
Design tokens, document-level element styling (links, lists, quotes, body defaults), the global spinner rules, the `light-dark` theming mixin, and an opt-in default dashboard palette.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```scss
|
|
8
|
+
// Base: css-reset + tokens + document styling (side-effectful)
|
|
9
|
+
@use '@greendrake/theme' as *;
|
|
10
|
+
|
|
11
|
+
// Opt-in: the default dashboard light/dark palette
|
|
12
|
+
@use '@greendrake/theme/dashboard-palette';
|
|
13
|
+
|
|
14
|
+
// App-specific palette instead:
|
|
15
|
+
@include light-dark(
|
|
16
|
+
(
|
|
17
|
+
--font-color: #222
|
|
18
|
+
// ...
|
|
19
|
+
),
|
|
20
|
+
(
|
|
21
|
+
--font-color: #eee
|
|
22
|
+
// ...
|
|
23
|
+
)
|
|
24
|
+
);
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Mixins exported by the root entry: `light-dark($light, $dark)`, `button-colors`, `overlay-icon-button($size)`, `wider-than-max-content` (plus `$max-content-width`). Framework-free SCSS primitives (breakpoints, z-layers, icon masks, font-face generation) live below this package in `@greendrake/scss-kit`.
|
|
28
|
+
|
|
29
|
+
## CSS custom property contract
|
|
30
|
+
|
|
31
|
+
`@greendrake/ui` components render correctly only when the custom properties below are defined. This package's two entries cover part of the contract; the rest is the consuming app's responsibility (typically set in its palette maps or `:root`).
|
|
32
|
+
|
|
33
|
+
### Defined by `@greendrake/theme` (root entry)
|
|
34
|
+
|
|
35
|
+
| Property | Value | Notes |
|
|
36
|
+
| --- | --- | --- |
|
|
37
|
+
| `--space-1` … `--space-6` | 4/8/12/16/24/28px | spacing scale; `--space-1`…`--space-4` are read by ui components |
|
|
38
|
+
| `--font-xs` … `--font-2xl` | 11/13/16/20/24/32px | font-size scale; `--font-xs` and `--font-md` are read by ui components |
|
|
39
|
+
| `--overlay-button-bg`, `--overlay-button-bg-hover` | `rgb(0 0 0 / 60%)`, `rgb(0 0 0 / 85%)` | consumed by the `overlay-icon-button` mixin |
|
|
40
|
+
| `--error-color` | `#ff4800` | semantic status color for app styling (not read by ui components) |
|
|
41
|
+
| `--warning-color` | `#890095` | semantic status color for app styling (not read by ui components) |
|
|
42
|
+
| `--accent-color` | `#0aa13e` | read by ui components (checkboxes, highlights) |
|
|
43
|
+
|
|
44
|
+
### Defined by `@greendrake/theme/dashboard-palette` (opt-in, light and dark values)
|
|
45
|
+
|
|
46
|
+
| Property | Light | Dark |
|
|
47
|
+
| --- | --- | --- |
|
|
48
|
+
| `--font-color` | `#222` | `#eee` |
|
|
49
|
+
| `--background-color` | `#ddd` | `#222` |
|
|
50
|
+
| `--form-background` | `#f5f5f5` | `#222` |
|
|
51
|
+
| `--form-background-hover` | `#fff` | `#000` |
|
|
52
|
+
| `--border-color` | `#555` | `#aaa` |
|
|
53
|
+
| `--border-color-light` | `#999` | `#666` |
|
|
54
|
+
| `--border-color-hover` | `#000` | `#fff` |
|
|
55
|
+
| `--link-color-hover` | `#000` | `#fff` |
|
|
56
|
+
| `--light-switch` | moonstar icon SVG | sun icon SVG |
|
|
57
|
+
| `--spinner-color` | `#ccc` | `#ccc` |
|
|
58
|
+
| `--page-background` | `#e6e6e6` | `#202020` |
|
|
59
|
+
| `--page-vignette-color` | `#cdcdcd` | `#0c0c0c` |
|
|
60
|
+
|
|
61
|
+
The `--page-*` pair feeds the `page-vignette` mixin (a flat canvas tone with a box-shadow edge falloff — deliberately not a radial gradient, whose page-sized form Firefox can mis-render as a bright dot at its centre). The dashboard preset emits it; an app on a palette of its own defines the pair and includes the mixin itself.
|
|
62
|
+
|
|
63
|
+
Apps not using the dashboard palette must define at least the `--font-color`/`--background-color`/`--form-background*`/`--border-color*`/`--spinner-color` group themselves — ui components read them with no fallback.
|
|
64
|
+
|
|
65
|
+
### Required from the consuming app (defined by neither entry, read with no fallback)
|
|
66
|
+
|
|
67
|
+
| Property | Read by |
|
|
68
|
+
| --- | --- |
|
|
69
|
+
| `--border-radius` | form controls, modals, tables, `overlay-icon-button` |
|
|
70
|
+
| `--icon-size` | `@greendrake/scss-kit` `icon` mixin, icon-bearing ui components |
|
|
71
|
+
| `--input-background` | text inputs |
|
|
72
|
+
| `--line-height-normal` | text layout in ui components |
|
|
73
|
+
| `--modal-background` | modal window |
|
|
74
|
+
| `--modal-border-radius` | modal window |
|
|
75
|
+
| `--spinner-side-color` | global `.spinner` rule, `img-spinner` mixin (typically `var(--form-background)`) |
|
|
76
|
+
| `--link-color` | document link styling in this package |
|
|
77
|
+
| `--blockquote-colour` | blockquote styling in this package |
|
|
78
|
+
| `--button-background` | `button-colors` mixin |
|
|
79
|
+
|
|
80
|
+
### Optional (ui components read them with a fallback)
|
|
81
|
+
|
|
82
|
+
`--cell-padding`, `--circular-progress-arc`, `--circular-progress-label`, `--circular-progress-track`, `--list-row-padding`, `--modal-backdrop-filter`, `--modal-box-shadow`, `--modal-button-min-width`, `--modal-max-width`, `--modal-window-backdrop-filter`, `--row-border-bottom`, `--row-select-color`, `--row-select-color-hover`, `--tree-line-color`.
|
|
83
|
+
|
|
84
|
+
Component-scoped properties with local defaults (`--checkbox-size`, `--switch-width`, `--switch-height`, `--switch-knob`) are documented per component in `@greendrake/ui`.
|
|
85
|
+
|
|
86
|
+
## Global spinner contract
|
|
87
|
+
|
|
88
|
+
This package defines the `spinner` keyframes and the `.spinner` / `body.spinner` / `div.spinner` rules that `@greendrake/ui`'s async controls and `@greendrake/scss-kit`'s `img-spinner` mixin rely on. They consume `--spinner-color` (palette) and `--spinner-side-color` (app).
|
|
89
|
+
|
|
90
|
+
## Not included
|
|
91
|
+
|
|
92
|
+
Tooltip (tippy) skin and select-control skin ship with the components that need them in `@greendrake/ui`; this package styles only plain document elements.
|
package/_index.scss
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
// This entry EMITS global CSS (the reset, base element styles, the :root
|
|
2
|
+
// design tokens). It is the app-level base — `@use` it ONCE from the app's
|
|
3
|
+
// main.scss. A component that needs only a mixin must `@use
|
|
4
|
+
// '@greendrake/theme/mixins'` (side-effect-free); importing this module from a
|
|
5
|
+
// per-component <style> re-emits the reset into a late stylesheet that
|
|
6
|
+
// overrides the app's own form-control skin.
|
|
7
|
+
@use '@greendrake/css-reset';
|
|
8
|
+
@use '@greendrake/scss-kit' as kit;
|
|
9
|
+
@use 'mixins';
|
|
10
|
+
@forward 'mixins';
|
|
11
|
+
|
|
12
|
+
:root {
|
|
13
|
+
// Feeds ui's TheWrap width cap; single-sourced from the SCSS constant the
|
|
14
|
+
// wider-than-max-content mixin also uses.
|
|
15
|
+
--max-content-width: #{mixins.$max-content-width};
|
|
16
|
+
--space-1: 4px;
|
|
17
|
+
--space-2: 8px;
|
|
18
|
+
--space-3: 12px;
|
|
19
|
+
--space-4: 16px;
|
|
20
|
+
--space-5: 24px;
|
|
21
|
+
--space-6: 28px;
|
|
22
|
+
--font-xs: 11px;
|
|
23
|
+
--font-sm: 13px;
|
|
24
|
+
--font-md: 16px;
|
|
25
|
+
--font-lg: 20px;
|
|
26
|
+
--font-xl: 24px;
|
|
27
|
+
--font-2xl: 32px;
|
|
28
|
+
|
|
29
|
+
// The safe-area insets, as custom properties because that is the only form
|
|
30
|
+
// both platforms can deliver — everything downstream reads these and stays
|
|
31
|
+
// platform-agnostic. iOS and browsers resolve them from env() right here.
|
|
32
|
+
// The Android shell cannot: Chromium's env() reports display cutouts alone,
|
|
33
|
+
// never the system bars, so it is 0 on a phone whose status bar merely
|
|
34
|
+
// overlays the WebView — which from Android 15 is every phone, the window
|
|
35
|
+
// being edge-to-edge with no way to opt out. There Capacitor's built-in
|
|
36
|
+
// SystemBars plugin writes the real window insets onto <html> as inline
|
|
37
|
+
// styles of these same names, and inline beats this rule, so the override
|
|
38
|
+
// needs nothing declared for it. (It writes 0 in the case where it has
|
|
39
|
+
// already padded the WebView itself — either way these stay the insets the
|
|
40
|
+
// page must still spend.)
|
|
41
|
+
--safe-area-inset-top: env(safe-area-inset-top, 0px);
|
|
42
|
+
--safe-area-inset-right: env(safe-area-inset-right, 0px);
|
|
43
|
+
--safe-area-inset-bottom: env(safe-area-inset-bottom, 0px);
|
|
44
|
+
--safe-area-inset-left: env(safe-area-inset-left, 0px);
|
|
45
|
+
|
|
46
|
+
--overlay-button-bg: rgb(0 0 0 / 60%);
|
|
47
|
+
--overlay-button-bg-hover: rgb(0 0 0 / 85%);
|
|
48
|
+
|
|
49
|
+
--error-color: #ff4800;
|
|
50
|
+
--warning-color: #890095;
|
|
51
|
+
--accent-color: #0aa13e;
|
|
52
|
+
|
|
53
|
+
// Single source of truth for spinner ring size — the default centred
|
|
54
|
+
// spinner shown at the middle of a loading UI. The small in-control variant
|
|
55
|
+
// overrides this (see button.spinner in form-controls).
|
|
56
|
+
--spinner-size: 40px;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
body {
|
|
60
|
+
color: var(--font-color);
|
|
61
|
+
font-size: var(--font-md);
|
|
62
|
+
min-height: 100dvh;
|
|
63
|
+
display: flex;
|
|
64
|
+
justify-content: center;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
p,
|
|
68
|
+
ol,
|
|
69
|
+
ul,
|
|
70
|
+
li {
|
|
71
|
+
margin: 6px 0;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
p {
|
|
75
|
+
font-size: var(--font-md);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
a,
|
|
79
|
+
a span,
|
|
80
|
+
a em {
|
|
81
|
+
color: var(--link-color);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
a[href^='http'] {
|
|
85
|
+
padding-inline-end: 18px;
|
|
86
|
+
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24'%3E%3Cg stroke='%23666' stroke-width='1.5' fill='none' fill-rule='evenodd' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='17 13.5 17 19.5 5 19.5 5 7.5 11 7.5'%3E%3C/polyline%3E%3Cpath d='M14,4.5 L20,4.5 L20,10.5 M20,4.5 L11,13.5'%3E%3C/path%3E%3C/g%3E%3C/svg%3E");
|
|
87
|
+
background-repeat: no-repeat;
|
|
88
|
+
// background-position takes no logical keywords, so the RTL side is stated.
|
|
89
|
+
background-position: right center;
|
|
90
|
+
background-size: 16px;
|
|
91
|
+
[dir='rtl'] & {
|
|
92
|
+
background-position: left center;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
ul {
|
|
97
|
+
list-style-type: square;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
ol,
|
|
101
|
+
ul {
|
|
102
|
+
padding-inline-start: 32px;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
blockquote {
|
|
106
|
+
border-inline-start: 8px solid #b8b5b9;
|
|
107
|
+
padding-inline-start: var(--space-4);
|
|
108
|
+
margin-inline-start: var(--space-4);
|
|
109
|
+
color: var(--blockquote-colour);
|
|
110
|
+
p,
|
|
111
|
+
em,
|
|
112
|
+
strong,
|
|
113
|
+
li {
|
|
114
|
+
color: var(--blockquote-colour);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.withUnderQuote p:last-child {
|
|
119
|
+
color: var(--font-color);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.hidden {
|
|
123
|
+
visibility: hidden;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.display-none {
|
|
127
|
+
display: none;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.contents {
|
|
131
|
+
display: contents;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.disabled,
|
|
135
|
+
[inert] {
|
|
136
|
+
@include kit.disabled;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
@keyframes spinner {
|
|
140
|
+
to {
|
|
141
|
+
transform: rotate(360deg);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.spinner:not(.splash) {
|
|
146
|
+
position: relative;
|
|
147
|
+
&::before {
|
|
148
|
+
content: '';
|
|
149
|
+
box-sizing: border-box;
|
|
150
|
+
position: absolute;
|
|
151
|
+
border-radius: 50%;
|
|
152
|
+
z-index: kit.z('spinner');
|
|
153
|
+
border: 2px solid var(--spinner-color);
|
|
154
|
+
border-top-color: var(--spinner-side-color);
|
|
155
|
+
animation: spinner 0.6s linear infinite;
|
|
156
|
+
top: 50%;
|
|
157
|
+
left: 50%;
|
|
158
|
+
// Size from the single-source token, centred via negative
|
|
159
|
+
// half-margins. Applies to any host element — div, section, aside,
|
|
160
|
+
// ul — rather than a fixed list of them.
|
|
161
|
+
width: var(--spinner-size);
|
|
162
|
+
height: var(--spinner-size);
|
|
163
|
+
margin-top: calc(var(--spinner-size) / -2);
|
|
164
|
+
margin-left: calc(var(--spinner-size) / -2);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Full-page spinner: pin to the viewport centre rather than the host element.
|
|
169
|
+
// Size and centring come from the base rule above. body::before belongs to
|
|
170
|
+
// this spinner alone — the page vignette hangs off html::before precisely so
|
|
171
|
+
// the two can never share one pseudo-element (page-vignette in _mixins.scss).
|
|
172
|
+
body:not(.splash).spinner::before {
|
|
173
|
+
position: fixed;
|
|
174
|
+
}
|
package/_mixins.scss
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
@use '@greendrake/scss-kit' as kit;
|
|
2
|
+
|
|
3
|
+
$max-content-width: 850px;
|
|
4
|
+
|
|
5
|
+
// Applies the $light / $dark custom-property maps under the .light/.dark :root
|
|
6
|
+
// classes, falling back to prefers-color-scheme when neither class is set.
|
|
7
|
+
@mixin light-dark($light, $dark) {
|
|
8
|
+
:root.light {
|
|
9
|
+
@include kit.apply-props($light);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
@media screen and (prefers-color-scheme: light) {
|
|
13
|
+
:root:not(.dark) {
|
|
14
|
+
@include kit.apply-props($light);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
:root.dark {
|
|
19
|
+
@include kit.apply-props($dark);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
@media screen and (prefers-color-scheme: dark) {
|
|
23
|
+
:root:not(.light) {
|
|
24
|
+
@include kit.apply-props($dark);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Page-canvas vignette: a flat tone at the centre falling off to a darker
|
|
30
|
+
// edge tone, painted with an inset box shadow on a fixed overlay rather than
|
|
31
|
+
// a radial gradient — Firefox's WebRender can mis-render the singular centre
|
|
32
|
+
// pixel of a page-sized radial gradient as a stray bright dot that comes and
|
|
33
|
+
// goes with window moves/resizes; a shadow has no degenerate point. Reads
|
|
34
|
+
// --page-background / --page-vignette-color from the palette.
|
|
35
|
+
//
|
|
36
|
+
// It hangs off <html>, not <body>: an element has exactly one ::before, and
|
|
37
|
+
// body's belongs to the full-page loading spinner (`body.spinner::before`, see
|
|
38
|
+
// _index.scss). Sharing it made the two fight — the spinner ring inherited the
|
|
39
|
+
// vignette's page-sized inset shadow, and the vignette vanished for the
|
|
40
|
+
// duration of every page load. The canvas is html's job anyway; it already
|
|
41
|
+
// carries the page background above.
|
|
42
|
+
@mixin page-vignette {
|
|
43
|
+
html {
|
|
44
|
+
// Also covers the overscroll gutter, which no fixed layer reaches.
|
|
45
|
+
background-color: var(--page-background);
|
|
46
|
+
&::before {
|
|
47
|
+
content: '';
|
|
48
|
+
position: fixed;
|
|
49
|
+
inset: 0;
|
|
50
|
+
z-index: -1;
|
|
51
|
+
pointer-events: none;
|
|
52
|
+
@include page-canvas;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// The canvas as a paintable layer: flat tone plus vignette. Anything that must
|
|
58
|
+
// READ as the page rather than as a panel paints this over a viewport-anchored
|
|
59
|
+
// box (`position: fixed; inset: 0`) and then masks itself down to the part it
|
|
60
|
+
// needs — page-scrim below is that construction. Same construction over the
|
|
61
|
+
// same box as the canvas above, so the painted region matches the page pixel
|
|
62
|
+
// for pixel, vignette falloff included, and its edges are the mask's business
|
|
63
|
+
// alone.
|
|
64
|
+
// The blur is sized against the SHORT viewport side, and modestly: a radius
|
|
65
|
+
// approaching the box's own width leaves the falloffs from opposite edges
|
|
66
|
+
// overlapping everywhere, so the gradient washes out into a flat tone — and
|
|
67
|
+
// engines diverge sharply in that saturated regime (WebKit floods the layer
|
|
68
|
+
// with the shadow colour outright, which is what a phone shows). 30vmin keeps
|
|
69
|
+
// every viewport shape well inside the range Chromium and WebKit rasterise
|
|
70
|
+
// identically.
|
|
71
|
+
@mixin page-canvas {
|
|
72
|
+
background-color: var(--page-background);
|
|
73
|
+
box-shadow: inset 0 0 30vmin var(--page-vignette-color);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// The backing for a bar stuck over scrolling content — a sticky header or
|
|
77
|
+
// footer — that must not read as a panel bolted onto the page. It is not a
|
|
78
|
+
// colour of its own but a second copy of the page canvas on its own
|
|
79
|
+
// viewport-anchored box, masked down to the strip the bar occupies: where it is
|
|
80
|
+
// opaque it is indistinguishable from the page behind it, vignette falloff
|
|
81
|
+
// included. Sits behind the bar's own content (z-index -1 inside the stacking
|
|
82
|
+
// context the bar's z-index opens), so nothing about the bar's children moves.
|
|
83
|
+
//
|
|
84
|
+
// $edge is the viewport edge the bar is stuck to. The mask holds full opacity
|
|
85
|
+
// for $solid from that edge — the band the bar's content occupies — then takes
|
|
86
|
+
// $ramp to reach clear, so content scrolls into view behind the bar with no
|
|
87
|
+
// edge to see.
|
|
88
|
+
@mixin page-scrim($edge, $solid, $ramp) {
|
|
89
|
+
@if $edge != top and $edge != bottom {
|
|
90
|
+
@error 'page-scrim: $edge must be top or bottom, got #{$edge}';
|
|
91
|
+
}
|
|
92
|
+
// The gradient runs from the far edge toward $edge, so its opaque stop
|
|
93
|
+
// lands on the bar.
|
|
94
|
+
$away: top;
|
|
95
|
+
@if $edge == top {
|
|
96
|
+
$away: bottom;
|
|
97
|
+
}
|
|
98
|
+
&::before {
|
|
99
|
+
content: '';
|
|
100
|
+
position: fixed;
|
|
101
|
+
inset: 0;
|
|
102
|
+
z-index: -1;
|
|
103
|
+
pointer-events: none;
|
|
104
|
+
@include page-canvas;
|
|
105
|
+
mask-image: linear-gradient(to #{$away}, #000 #{$solid}, transparent calc(#{$solid} + #{$ramp}));
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
@mixin button-colors {
|
|
110
|
+
background: var(--button-background);
|
|
111
|
+
&:hover {
|
|
112
|
+
filter: brightness(120%);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
@mixin wider-than-max-content {
|
|
117
|
+
@media (min-width: $max-content-width) {
|
|
118
|
+
@content;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
@mixin overlay-icon-button($size: 24px) {
|
|
123
|
+
position: absolute;
|
|
124
|
+
width: $size;
|
|
125
|
+
height: $size;
|
|
126
|
+
border-radius: var(--border-radius);
|
|
127
|
+
background: var(--overlay-button-bg);
|
|
128
|
+
opacity: 0.5;
|
|
129
|
+
color: #fff;
|
|
130
|
+
border: 0;
|
|
131
|
+
padding: 0;
|
|
132
|
+
cursor: pointer;
|
|
133
|
+
line-height: $size;
|
|
134
|
+
font-size: calc($size * 0.55);
|
|
135
|
+
&:hover {
|
|
136
|
+
opacity: 1;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Default dashboard palette: opt-in via `@use '@greendrake/theme/dashboard-palette';`.
|
|
2
|
+
@use '@greendrake/scss-kit' as kit;
|
|
3
|
+
@use 'mixins' as *;
|
|
4
|
+
|
|
5
|
+
@include light-dark(
|
|
6
|
+
(
|
|
7
|
+
--font-color: #222,
|
|
8
|
+
--background-color: #ddd,
|
|
9
|
+
--form-background: #f5f5f5,
|
|
10
|
+
--form-background-hover: #fff,
|
|
11
|
+
--border-color: #555,
|
|
12
|
+
--border-color-light: #999,
|
|
13
|
+
--border-color-hover: #000,
|
|
14
|
+
--link-color-hover: #000,
|
|
15
|
+
--light-switch: kit.icon-svg('moonstar'),
|
|
16
|
+
--spinner-color: #ccc,
|
|
17
|
+
--page-background: #e6e6e6,
|
|
18
|
+
--page-vignette-color: #cdcdcd
|
|
19
|
+
),
|
|
20
|
+
(
|
|
21
|
+
--font-color: #eee,
|
|
22
|
+
--background-color: #222,
|
|
23
|
+
--form-background: #222,
|
|
24
|
+
--form-background-hover: #000,
|
|
25
|
+
--border-color: #aaa,
|
|
26
|
+
--border-color-light: #666,
|
|
27
|
+
--border-color-hover: #fff,
|
|
28
|
+
--link-color-hover: #fff,
|
|
29
|
+
--light-switch: kit.icon-svg('sun'),
|
|
30
|
+
--spinner-color: #ccc,
|
|
31
|
+
--page-background: #202020,
|
|
32
|
+
--page-vignette-color: #0c0c0c
|
|
33
|
+
)
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
// Modal chrome vars consumed by @greendrake/ui's Modal — part of the default
|
|
37
|
+
// dashboard look (the dark-glass window over a dimmed backdrop).
|
|
38
|
+
:root {
|
|
39
|
+
--modal-background: var(--background-color);
|
|
40
|
+
--modal-box-shadow: rgba(255, 255, 255, 0.5) 0px 4px 25px;
|
|
41
|
+
--modal-border-radius: var(--border-radius);
|
|
42
|
+
--modal-backdrop-filter: brightness(0.4);
|
|
43
|
+
--modal-window-backdrop-filter: blur(8px);
|
|
44
|
+
}
|
package/dashboard.scss
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// The full dashboard-app preset: palette + native-control skin + editor-row
|
|
2
|
+
// layout + the shell styling a dashboard would otherwise write for itself.
|
|
3
|
+
// Opt in via
|
|
4
|
+
// `@use '@greendrake/theme/dashboard';` (after `@use '@greendrake/theme'`).
|
|
5
|
+
@use 'dashboard-palette';
|
|
6
|
+
@use 'form-controls';
|
|
7
|
+
@use 'editor-rows';
|
|
8
|
+
@use 'mixins' as *;
|
|
9
|
+
|
|
10
|
+
@include page-vignette;
|
|
11
|
+
|
|
12
|
+
:root {
|
|
13
|
+
--border-radius: 4px;
|
|
14
|
+
--icon-size: 18px;
|
|
15
|
+
// Denser base than the document default: dashboards are dense,
|
|
16
|
+
// information-first surfaces.
|
|
17
|
+
--font-md: 14px;
|
|
18
|
+
// Required by the ui .spinner ring contract (see README).
|
|
19
|
+
--spinner-side-color: var(--form-background);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
html,
|
|
23
|
+
body {
|
|
24
|
+
height: 100vh;
|
|
25
|
+
overflow: hidden;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
body {
|
|
29
|
+
padding: 0;
|
|
30
|
+
margin: 0;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
a:hover {
|
|
34
|
+
color: var(--link-color-hover);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
label {
|
|
38
|
+
font-size: var(--font-xs);
|
|
39
|
+
font-weight: 500;
|
|
40
|
+
opacity: 0.7;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Headings read as panel/section dividers rather than page titles — every
|
|
44
|
+
// dashboard panel opens with one, and detail views nest h2/h3 under it.
|
|
45
|
+
h1,
|
|
46
|
+
h2,
|
|
47
|
+
h3 {
|
|
48
|
+
font-weight: 500;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
h1 {
|
|
52
|
+
font-size: var(--font-2xl);
|
|
53
|
+
line-height: 36px;
|
|
54
|
+
margin: var(--space-5) 0 var(--space-3);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
h2 {
|
|
58
|
+
border-bottom: 1px solid var(--border-color-light);
|
|
59
|
+
margin: var(--space-4) 0 var(--space-2);
|
|
60
|
+
opacity: 0.8;
|
|
61
|
+
font-size: var(--font-xl);
|
|
62
|
+
line-height: 28px;
|
|
63
|
+
}
|
package/editor-rows.scss
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Opt-in record-editor/detail-pane layout: `@use '@greendrake/theme/editor-rows';`
|
|
2
|
+
// (the dashboard preset pulls it in). `.EditorRows` is the padded scrolling
|
|
3
|
+
// pane, `.EditorRows__row` a label/value line. The classes are independent:
|
|
4
|
+
// a component whose root layout diverges may adopt the rows alone.
|
|
5
|
+
|
|
6
|
+
.EditorRows {
|
|
7
|
+
padding: var(--space-4);
|
|
8
|
+
overflow: auto;
|
|
9
|
+
height: 100%;
|
|
10
|
+
box-sizing: border-box;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.EditorRows__row {
|
|
14
|
+
display: flex;
|
|
15
|
+
gap: var(--space-3);
|
|
16
|
+
padding: var(--space-2) 0;
|
|
17
|
+
border-bottom: 1px solid var(--border-color-light);
|
|
18
|
+
> label {
|
|
19
|
+
min-width: 160px;
|
|
20
|
+
font-size: var(--font-xs);
|
|
21
|
+
opacity: 0.7;
|
|
22
|
+
padding-top: 2px;
|
|
23
|
+
}
|
|
24
|
+
> div {
|
|
25
|
+
flex-grow: 1;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Opt-in native form-control skin: `@use '@greendrake/theme/form-controls';`.
|
|
2
|
+
// The block a dashboard would otherwise write into its own main.scss.
|
|
3
|
+
// Relies on the palette vars (--form-background, --border-color*, --font-color)
|
|
4
|
+
// plus --border-radius, which stays app-supplied (see README).
|
|
5
|
+
|
|
6
|
+
button,
|
|
7
|
+
input,
|
|
8
|
+
select,
|
|
9
|
+
textarea {
|
|
10
|
+
border-radius: var(--border-radius);
|
|
11
|
+
color: var(--font-color);
|
|
12
|
+
background: var(--form-background);
|
|
13
|
+
border: 1px solid var(--border-color);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
button {
|
|
17
|
+
cursor: pointer;
|
|
18
|
+
padding: var(--space-1) var(--space-4);
|
|
19
|
+
&:hover:not(:disabled) {
|
|
20
|
+
border-color: var(--border-color-hover);
|
|
21
|
+
background-color: var(--form-background-hover);
|
|
22
|
+
}
|
|
23
|
+
&:disabled {
|
|
24
|
+
opacity: 0.5;
|
|
25
|
+
cursor: default;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
input,
|
|
30
|
+
select,
|
|
31
|
+
textarea {
|
|
32
|
+
padding: var(--space-1) var(--space-2);
|
|
33
|
+
&:focus {
|
|
34
|
+
outline: none;
|
|
35
|
+
border-color: var(--border-color-hover);
|
|
36
|
+
background-color: var(--form-background-hover);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Small in-control variant of the theme's .spinner ring (action buttons while
|
|
41
|
+
// their async action is in flight): the sole size knob is --spinner-size, so the
|
|
42
|
+
// ring geometry stays owned by the base rule.
|
|
43
|
+
button.spinner {
|
|
44
|
+
pointer-events: none;
|
|
45
|
+
cursor: default;
|
|
46
|
+
color: transparent !important;
|
|
47
|
+
--spinner-size: 24px;
|
|
48
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@greendrake/theme",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Design tokens, document-level element styling, a light/dark theming mixin and an opt-in dashboard palette",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Eugene Greendrake <eugene@greendrake.nz>",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/greendrake/lib.git",
|
|
10
|
+
"directory": "theme"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/greendrake/lib/tree/main/theme#readme",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/greendrake/lib/issues"
|
|
15
|
+
},
|
|
16
|
+
"type": "module",
|
|
17
|
+
"main": "_index.scss",
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"sass": "./_index.scss",
|
|
21
|
+
"default": "./_index.scss"
|
|
22
|
+
},
|
|
23
|
+
"./mixins": {
|
|
24
|
+
"sass": "./_mixins.scss",
|
|
25
|
+
"default": "./_mixins.scss"
|
|
26
|
+
},
|
|
27
|
+
"./dashboard-palette": {
|
|
28
|
+
"sass": "./dashboard-palette.scss",
|
|
29
|
+
"default": "./dashboard-palette.scss"
|
|
30
|
+
},
|
|
31
|
+
"./form-controls": {
|
|
32
|
+
"sass": "./form-controls.scss",
|
|
33
|
+
"default": "./form-controls.scss"
|
|
34
|
+
},
|
|
35
|
+
"./editor-rows": {
|
|
36
|
+
"sass": "./editor-rows.scss",
|
|
37
|
+
"default": "./editor-rows.scss"
|
|
38
|
+
},
|
|
39
|
+
"./dashboard": {
|
|
40
|
+
"sass": "./dashboard.scss",
|
|
41
|
+
"default": "./dashboard.scss"
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"files": [
|
|
45
|
+
"_index.scss",
|
|
46
|
+
"_mixins.scss",
|
|
47
|
+
"dashboard-palette.scss",
|
|
48
|
+
"form-controls.scss",
|
|
49
|
+
"editor-rows.scss",
|
|
50
|
+
"dashboard.scss"
|
|
51
|
+
],
|
|
52
|
+
"publishConfig": {
|
|
53
|
+
"access": "public"
|
|
54
|
+
},
|
|
55
|
+
"prettier": "@greendrake/dev-config/prettier",
|
|
56
|
+
"dependencies": {
|
|
57
|
+
"@greendrake/css-reset": "0.1.0",
|
|
58
|
+
"@greendrake/scss-kit": "0.1.0"
|
|
59
|
+
},
|
|
60
|
+
"devDependencies": {
|
|
61
|
+
"@greendrake/dev-config": "0.1.0"
|
|
62
|
+
}
|
|
63
|
+
}
|