gdk-toogle 0.9.3 → 1.0.1
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.
- checksums.yaml +4 -4
- data/README.md +76 -13
- data/Rakefile +4 -1
- data/app/assets/javascript/toogle/application.js +184 -49
- data/app/assets/stylesheets/toogle/application.css +1 -3
- data/app/assets/stylesheets/toogle/components/toggle.css +37 -22
- data/app/assets/stylesheets/toogle/elements.css +92 -23
- data/app/assets/stylesheets/toogle/layout.css +67 -24
- data/app/assets/stylesheets/toogle/utilities.css +5 -4
- data/app/assets/stylesheets/toogle/variables.css +43 -7
- data/app/controllers/toogle/application_controller.rb +2 -5
- data/app/controllers/toogle/definitions_controller.rb +1 -4
- data/app/controllers/toogle/features_controller.rb +14 -4
- data/app/models/toogle/definition.rb +9 -8
- data/app/models/toogle/feature.rb +4 -4
- data/app/views/layouts/toogle/application.html.haml +15 -10
- data/app/views/toogle/features/_details_button.html.haml +7 -0
- data/app/views/toogle/features/index.html.haml +93 -66
- data/lib/toogle/version.rb +1 -1
- metadata +4 -11
- data/app/assets/stylesheets/toogle/components/card.css +0 -10
- data/app/assets/stylesheets/toogle/components/scrollbox.css +0 -26
- data/app/views/toogle/definitions/index.html.haml +0 -9
- data/app/views/toogle/features/_dialog.html.haml +0 -18
- data/app/views/toogle/features/_toggle.html.haml +0 -8
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5b7fb4c9b37ce9f5712cc5635ff39bea2b3745179ea554dbe4b941f2a9472cab
|
|
4
|
+
data.tar.gz: 57a6c432b93d5dc0e88c52f46c653ca26338638f99f866c57e56dc5540a21692
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: da4f86f837651e78dc12c7c61e2563cb8e099ff5a6a454e00226728906427243649190743cc36b46bcac34967551542afe8da58f5f5a35e5c6fd834412d434e4
|
|
7
|
+
data.tar.gz: 807b565d41e6f131a0811fb7e4d980f1c5a0a5ad5a61c892e81f3c9e6d4c2c614fe269bfadf8087465467deca63d3297080366ad1827b4abd302a09c5fb30e21
|
data/README.md
CHANGED
|
@@ -4,30 +4,93 @@ A Rails engine web-UI to quickly toggle feature flags in GDK.
|
|
|
4
4
|
|
|
5
5
|
## Usage
|
|
6
6
|
|
|
7
|
-
This gem was written specifically for the GitLab codebase. It
|
|
7
|
+
This gem was written specifically for the GitLab codebase. It relies on
|
|
8
|
+
GitLab's `Feature` classes, so mounting it in any other Rails app won't work
|
|
9
|
+
(for developing Toogle itself there is a standalone dummy app, see below).
|
|
8
10
|
|
|
9
11
|
### Usage within the GitLab Development Kit
|
|
10
12
|
|
|
11
|
-
|
|
13
|
+
The UI is available at http://gdk.test:3000/rails/features.
|
|
12
14
|
|
|
13
|
-
|
|
14
|
-
- Add `gem 'toogle', path: 'local_toogle_path_here'` to the Gemfile and `bundle`.
|
|
15
|
-
- Add `mount Toogle::Engine, at: '/rails/toogle'` to `config/routes/development.rb`.
|
|
16
|
-
- Restart the rails server to pick up the new route: `gdk restart rails-web`
|
|
15
|
+
## Contributing
|
|
17
16
|
|
|
18
|
-
|
|
17
|
+
Everyone can contribute. Open issues and merge requests at
|
|
18
|
+
https://gitlab.com/thutterer/toogle. Please make sure `rake` (specs and
|
|
19
|
+
linter) passes before submitting.
|
|
19
20
|
|
|
20
|
-
|
|
21
|
+
There is an `AGENTS.md` with the conventions and architecture notes that
|
|
22
|
+
keep this codebase simple — worth a read, and keep it accurate when you
|
|
23
|
+
change things.
|
|
24
|
+
|
|
25
|
+
### How to run it standalone
|
|
26
|
+
|
|
27
|
+
The dummy app used for testing includes fake in-memory versions of GitLab's
|
|
28
|
+
`Feature` and Flipper classes (see `spec/dummy/app/models/`), so you can
|
|
29
|
+
develop the engine without a GDK:
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
cd spec/dummy
|
|
33
|
+
bin/rails server
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Then open http://localhost:3000/toogle. Toggled state lives in memory and
|
|
37
|
+
resets on restart.
|
|
38
|
+
|
|
39
|
+
To test your changes inside a real GDK, point GitLab's `Gemfile` at your
|
|
40
|
+
local checkout:
|
|
41
|
+
|
|
42
|
+
```ruby
|
|
43
|
+
gem 'gdk-toogle', path: '~/Code/toogle', require: 'toogle'
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### How to run tests and linting
|
|
47
|
+
|
|
48
|
+
Run `rake` to run both the specs and the [standard](https://github.com/standardrb/standard)
|
|
49
|
+
linter. They are also available separately as `rake spec` and `rake standard`
|
|
50
|
+
(use `rake standard:fix` to autoformat).
|
|
51
|
+
|
|
52
|
+
To lint automatically before each commit, enable the bundled git hook once:
|
|
53
|
+
|
|
54
|
+
```sh
|
|
55
|
+
git config core.hooksPath .githooks
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The system specs in `spec/system` use headless Firefox by default. Set
|
|
59
|
+
`CAPYBARA_DRIVER` to any registered Capybara driver to switch, e.g. if you
|
|
60
|
+
don't have Firefox or only a Snap-installed one (incompatible with
|
|
61
|
+
geckodriver):
|
|
62
|
+
|
|
63
|
+
```sh
|
|
64
|
+
CAPYBARA_DRIVER=selenium_chrome_headless bundle exec rspec
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### How to cut a release
|
|
68
|
+
|
|
69
|
+
1. Bump `Toogle::VERSION` in `lib/toogle/version.rb`.
|
|
70
|
+
2. Add an entry to `CHANGELOG.md`.
|
|
71
|
+
3. Commit, then tag: `git tag vX.Y.Z && git push --tags`.
|
|
72
|
+
4. Build and push the gem: `rake build` then `gem push pkg/gdk-toogle-X.Y.Z.gem`.
|
|
73
|
+
5. Bump the version constraint in GitLab's `Gemfile` to pick it up.
|
|
74
|
+
|
|
75
|
+
Keep Ruby and gem versions at or below what GitLab itself runs (see
|
|
76
|
+
`AGENTS.md`).
|
|
77
|
+
|
|
78
|
+
## Tech stack
|
|
79
|
+
|
|
80
|
+
One design goal of this little Rails engine was to keep it free of any dependencies, that GitLab uses today.
|
|
81
|
+
Because, tomorrow we might decide to ditch one or the other tech we use today, and Toogle shouldn't break just because GitLab removes `sassc`, as an example (that actually happened during development).
|
|
82
|
+
|
|
83
|
+
That's why Toogle only depends on the bare basics: `rails`, `sprockets` and `haml`.
|
|
21
84
|
|
|
22
|
-
|
|
85
|
+
### Javascript
|
|
23
86
|
|
|
24
|
-
|
|
87
|
+
Toogle uses [Alpine.js](https://alpinejs.dev/) from a `<script>` tag so it does not use or depend on our Vue.js usage or any bundler. Alpine is very similar to Vue, so it's really easy to pick up. Give it a try!
|
|
25
88
|
|
|
26
|
-
|
|
89
|
+
### CSS
|
|
27
90
|
|
|
28
|
-
|
|
91
|
+
Toogle uses native unprocessed CSS to not depend on any specific preprocessor. Modern CSS is awesome, and all modern browsers support things like nested selectors today.
|
|
29
92
|
|
|
30
|
-
|
|
93
|
+
For theming and dark mode, it uses CSS variables and HSL colors, which makes it super easy to switch from light to dark by just changing the variable that makes up the _Lightness_. Check it out!
|
|
31
94
|
|
|
32
95
|
## License
|
|
33
96
|
|
data/Rakefile
CHANGED
|
@@ -1,10 +1,41 @@
|
|
|
1
1
|
document.addEventListener("alpine:init", () => {
|
|
2
|
-
Alpine.
|
|
2
|
+
Alpine.store("flash", {
|
|
3
|
+
message: "",
|
|
4
|
+
|
|
5
|
+
show(message) {
|
|
6
|
+
this.message = message;
|
|
7
|
+
},
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
const showFetchError = (error) => {
|
|
11
|
+
console.error("Error fetching data:", error);
|
|
12
|
+
Alpine.store("flash").show(
|
|
13
|
+
"Could not reach the server. Is your GDK running?"
|
|
14
|
+
);
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const csrfToken = () =>
|
|
18
|
+
document.head.querySelector("meta[name=csrf-token]")?.content;
|
|
19
|
+
|
|
20
|
+
// Cap how many Defaults render at once. This is a search tool — the wanted
|
|
21
|
+
// flag is reachable by typing — so we never build the full ~700+ row DOM.
|
|
22
|
+
const MAX_DEFAULTS_SHOWN = 50;
|
|
23
|
+
|
|
24
|
+
Alpine.data("flags", (featuresUrl, definitionsUrl, shareName = "") => ({
|
|
3
25
|
features: [],
|
|
4
|
-
|
|
26
|
+
definitions: [],
|
|
27
|
+
query: "",
|
|
28
|
+
lastMoved: "",
|
|
29
|
+
moving: "",
|
|
30
|
+
selectedName: "",
|
|
31
|
+
max: MAX_DEFAULTS_SHOWN,
|
|
32
|
+
featuresUrl: featuresUrl.replace(/\/$/, ""),
|
|
33
|
+
definitionsUrl,
|
|
5
34
|
|
|
6
35
|
init() {
|
|
7
|
-
|
|
36
|
+
const featuresLoaded = this.fetchFeatures();
|
|
37
|
+
|
|
38
|
+
const definitionsLoaded = fetch(this.definitionsUrl, {
|
|
8
39
|
method: "GET",
|
|
9
40
|
headers: {
|
|
10
41
|
Accept: "application/json",
|
|
@@ -12,70 +43,174 @@ document.addEventListener("alpine:init", () => {
|
|
|
12
43
|
})
|
|
13
44
|
.then((response) => response.json())
|
|
14
45
|
.then((data) => {
|
|
15
|
-
this.
|
|
46
|
+
this.definitions = data;
|
|
16
47
|
})
|
|
17
|
-
.catch(
|
|
48
|
+
.catch(showFetchError);
|
|
49
|
+
|
|
50
|
+
if (shareName) {
|
|
51
|
+
Promise.all([featuresLoaded, definitionsLoaded]).then(() =>
|
|
52
|
+
this.openDetails(shareName)
|
|
53
|
+
);
|
|
54
|
+
}
|
|
18
55
|
},
|
|
19
56
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
57
|
+
openDetails(name) {
|
|
58
|
+
this.selectedName = name;
|
|
59
|
+
history.replaceState({}, "", `${this.featuresUrl}/${name}`);
|
|
60
|
+
this.$refs.dialog.showModal();
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
closeDetails() {
|
|
64
|
+
this.selectedName = "";
|
|
65
|
+
history.replaceState({}, "", `${this.featuresUrl}/`);
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
// The dialog's subject, looked up live so it stays in sync after toggles.
|
|
69
|
+
selected() {
|
|
70
|
+
const feature = this.features.find((f) => f.name === this.selectedName);
|
|
71
|
+
if (feature) {
|
|
72
|
+
return {
|
|
73
|
+
state: feature.state,
|
|
74
|
+
definition: feature.definition,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const definition = this.definitions.find(
|
|
79
|
+
(d) => d.name === this.selectedName
|
|
80
|
+
);
|
|
81
|
+
if (definition) {
|
|
82
|
+
return {
|
|
83
|
+
state: definition.default_enabled ? "enabled" : "disabled",
|
|
84
|
+
definition,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return null;
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
toggleSelected() {
|
|
92
|
+
const feature = this.features.find((f) => f.name === this.selectedName);
|
|
93
|
+
|
|
94
|
+
if (feature) {
|
|
95
|
+
this.toggleFeature(feature);
|
|
96
|
+
} else {
|
|
97
|
+
this.toggleDefault(this.selected().definition);
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
fetchFeatures(movedName = "") {
|
|
102
|
+
return fetch(this.featuresUrl, {
|
|
103
|
+
method: "GET",
|
|
24
104
|
headers: {
|
|
25
105
|
Accept: "application/json",
|
|
26
|
-
"Content-Type": "application/json",
|
|
27
106
|
},
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
107
|
+
})
|
|
108
|
+
.then((response) => response.json())
|
|
109
|
+
.then((data) => {
|
|
110
|
+
if (movedName) {
|
|
111
|
+
this.animated(() => {
|
|
112
|
+
this.features = data;
|
|
113
|
+
}, movedName);
|
|
114
|
+
} else {
|
|
115
|
+
// Not animated: the initial load should not "fly in" rows.
|
|
116
|
+
this.features = data;
|
|
117
|
+
}
|
|
118
|
+
})
|
|
119
|
+
.catch(showFetchError);
|
|
120
|
+
},
|
|
121
|
+
|
|
122
|
+
// Animates the layout change, e.g. a row moving between the sections.
|
|
123
|
+
// Only the moving row gets a view-transition-name (see the view), so the
|
|
124
|
+
// browser snapshots one row, not all ~700 — keeping toggles fast no
|
|
125
|
+
// matter how many flags exist. The moved row then gets a short "shine".
|
|
126
|
+
async animated(applyChange, movedName = "") {
|
|
127
|
+
if (!document.startViewTransition || !movedName) return applyChange();
|
|
128
|
+
|
|
129
|
+
// Name the moving row before the browser snapshots the old state.
|
|
130
|
+
this.moving = movedName;
|
|
131
|
+
await this.$nextTick();
|
|
132
|
+
|
|
133
|
+
const transition = document.startViewTransition(() => {
|
|
134
|
+
applyChange();
|
|
135
|
+
return this.$nextTick();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
await transition.finished.catch(() => {});
|
|
139
|
+
this.moving = "";
|
|
140
|
+
this.lastMoved = movedName;
|
|
141
|
+
setTimeout(() => {
|
|
142
|
+
if (this.lastMoved === movedName) this.lastMoved = "";
|
|
143
|
+
}, 1200);
|
|
144
|
+
},
|
|
145
|
+
|
|
146
|
+
filteredFeatures() {
|
|
147
|
+
return this.features.filter((f) => f.name.includes(this.query.trim()));
|
|
148
|
+
},
|
|
149
|
+
|
|
150
|
+
// All definitions matching the query, minus those already customized.
|
|
151
|
+
matchingDefinitions() {
|
|
152
|
+
const customized = this.features.map((f) => f.name);
|
|
153
|
+
return this.definitions.filter(
|
|
154
|
+
(d) =>
|
|
155
|
+
d.name.includes(this.query.trim()) && !customized.includes(d.name)
|
|
156
|
+
);
|
|
157
|
+
},
|
|
158
|
+
|
|
159
|
+
// Only the rows we actually render (capped); see MAX_DEFAULTS_SHOWN.
|
|
160
|
+
filteredDefinitions() {
|
|
161
|
+
return this.matchingDefinitions().slice(0, this.max);
|
|
162
|
+
},
|
|
163
|
+
|
|
164
|
+
defaultsCapped() {
|
|
165
|
+
return this.matchingDefinitions().length > this.max;
|
|
166
|
+
},
|
|
167
|
+
|
|
168
|
+
toggleFeature(feature) {
|
|
169
|
+
const newState = feature.state === "enabled" ? "disabled" : "enabled";
|
|
170
|
+
|
|
171
|
+
this.putState(feature.name, newState).then(() => {
|
|
32
172
|
this.features = this.features.map((f) => {
|
|
33
173
|
return f === feature ? { ...f, state: newState } : f;
|
|
34
174
|
});
|
|
35
175
|
});
|
|
36
176
|
},
|
|
37
177
|
|
|
38
|
-
|
|
39
|
-
const
|
|
40
|
-
"meta[name=csrf-token]"
|
|
41
|
-
)?.content;
|
|
178
|
+
toggleDefault(definition) {
|
|
179
|
+
const newState = definition.default_enabled ? "disabled" : "enabled";
|
|
42
180
|
|
|
43
|
-
|
|
44
|
-
|
|
181
|
+
this.putState(definition.name, newState).then(() =>
|
|
182
|
+
this.fetchFeatures(definition.name)
|
|
183
|
+
);
|
|
184
|
+
},
|
|
185
|
+
|
|
186
|
+
putState(featureName, state) {
|
|
187
|
+
return fetch(`${this.featuresUrl}/${featureName}`, {
|
|
188
|
+
method: "PUT",
|
|
45
189
|
headers: {
|
|
46
190
|
Accept: "application/json",
|
|
47
|
-
"
|
|
191
|
+
"Content-Type": "application/json",
|
|
192
|
+
"X-CSRF-Token": csrfToken(),
|
|
48
193
|
},
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
});
|
|
194
|
+
body: JSON.stringify({ state }),
|
|
195
|
+
}).catch(showFetchError);
|
|
52
196
|
},
|
|
53
|
-
}));
|
|
54
197
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
},
|
|
72
|
-
body: JSON.stringify({
|
|
73
|
-
state: this.checked ? "disabled" : "enabled",
|
|
74
|
-
}),
|
|
75
|
-
}).then(() => {
|
|
76
|
-
window.location = this.indexUrl;
|
|
77
|
-
});
|
|
78
|
-
},
|
|
198
|
+
deleteFeature(featureName) {
|
|
199
|
+
fetch(`${this.featuresUrl}/${featureName}`, {
|
|
200
|
+
method: "DELETE",
|
|
201
|
+
headers: {
|
|
202
|
+
Accept: "application/json",
|
|
203
|
+
"X-CSRF-Token": csrfToken(),
|
|
204
|
+
},
|
|
205
|
+
})
|
|
206
|
+
.then(() => {
|
|
207
|
+
this.animated(() => {
|
|
208
|
+
this.features = this.features.filter(
|
|
209
|
+
(f) => f.name !== featureName
|
|
210
|
+
);
|
|
211
|
+
}, featureName);
|
|
212
|
+
})
|
|
213
|
+
.catch(showFetchError);
|
|
79
214
|
},
|
|
80
215
|
}));
|
|
81
216
|
|
|
@@ -4,15 +4,13 @@
|
|
|
4
4
|
@import "layout";
|
|
5
5
|
@import "utilities";
|
|
6
6
|
|
|
7
|
-
@import "components/card";
|
|
8
|
-
@import "components/scrollbox";
|
|
9
7
|
@import "components/toggle";
|
|
10
8
|
|
|
11
9
|
[x-cloak] {
|
|
12
10
|
display: none !important;
|
|
13
11
|
}
|
|
14
12
|
|
|
15
|
-
|
|
13
|
+
.notice {
|
|
16
14
|
background-color: #fc6d26;
|
|
17
15
|
color: white;
|
|
18
16
|
padding: 0.5rem;
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
width: 60px;
|
|
5
5
|
height: 34px;
|
|
6
6
|
margin: 4px;
|
|
7
|
+
flex-shrink: 0;
|
|
7
8
|
|
|
8
9
|
input {
|
|
9
10
|
opacity: 0;
|
|
@@ -14,11 +15,9 @@
|
|
|
14
15
|
.handle {
|
|
15
16
|
position: absolute;
|
|
16
17
|
cursor: pointer;
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
bottom: 0;
|
|
21
|
-
background-color: #ccc;
|
|
18
|
+
inset: 0;
|
|
19
|
+
background-color: var(--color-track);
|
|
20
|
+
box-shadow: inset 0 1px 3px var(--color-shadow);
|
|
22
21
|
transition: 0.2s;
|
|
23
22
|
|
|
24
23
|
&:before {
|
|
@@ -28,10 +27,19 @@
|
|
|
28
27
|
width: 26px;
|
|
29
28
|
left: 4px;
|
|
30
29
|
bottom: 4px;
|
|
31
|
-
background-color:
|
|
30
|
+
background-color: var(--color-thumb);
|
|
31
|
+
box-shadow: 0 1px 2px var(--color-shadow);
|
|
32
32
|
transition: 0.2s;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
&:hover:before {
|
|
36
|
+
filter: brightness(1.1);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
&:active:before {
|
|
40
|
+
transform: scale(0.92);
|
|
41
|
+
}
|
|
42
|
+
|
|
35
43
|
&.round {
|
|
36
44
|
border-radius: 34px;
|
|
37
45
|
}
|
|
@@ -41,30 +49,37 @@
|
|
|
41
49
|
}
|
|
42
50
|
}
|
|
43
51
|
|
|
44
|
-
input:checked + .handle {
|
|
52
|
+
input:checked + .handle:before {
|
|
45
53
|
background-color: var(--color-primary);
|
|
54
|
+
transform: translateX(26px);
|
|
46
55
|
}
|
|
47
56
|
|
|
48
|
-
input:
|
|
49
|
-
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
input:checked + .handle:before {
|
|
53
|
-
-webkit-transform: translateX(26px);
|
|
54
|
-
-ms-transform: translateX(26px);
|
|
55
|
-
transform: translateX(26px);
|
|
57
|
+
input:checked + .handle:active:before {
|
|
58
|
+
transform: translateX(26px) scale(0.92);
|
|
56
59
|
}
|
|
57
60
|
|
|
58
|
-
input:
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
text-align: center;
|
|
62
|
-
color: white;
|
|
63
|
-
font-size: x-small;
|
|
64
|
-
line-height: 26px;
|
|
61
|
+
input:focus-visible + .handle {
|
|
62
|
+
outline: 2px solid var(--color-primary);
|
|
63
|
+
outline-offset: 2px;
|
|
65
64
|
}
|
|
66
65
|
|
|
66
|
+
/* Truly disabled (e.g. flag unknown on this branch): flat and washed out
|
|
67
|
+
into the page, nothing raised or sunken that could look clickable. */
|
|
67
68
|
input:disabled + .handle {
|
|
68
69
|
cursor: not-allowed;
|
|
70
|
+
background-color: var(--color-disabled);
|
|
71
|
+
box-shadow: none;
|
|
72
|
+
|
|
73
|
+
&:before,
|
|
74
|
+
&:hover:before {
|
|
75
|
+
content: "N/A";
|
|
76
|
+
text-align: center;
|
|
77
|
+
color: var(--color-text-muted);
|
|
78
|
+
font-size: x-small;
|
|
79
|
+
line-height: 26px;
|
|
80
|
+
background-color: transparent;
|
|
81
|
+
box-shadow: none;
|
|
82
|
+
filter: none;
|
|
83
|
+
}
|
|
69
84
|
}
|
|
70
85
|
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
+
* {
|
|
2
|
+
box-sizing: border-box;
|
|
3
|
+
}
|
|
4
|
+
|
|
1
5
|
html {
|
|
2
6
|
color: var(--color-text);
|
|
3
7
|
transition: 0.2s;
|
|
4
|
-
background-
|
|
5
|
-
circle at 10vh 10vw,
|
|
6
|
-
hsl(var(--hue-primary), var(--saturation-bg), var(--lightness-bg1)),
|
|
7
|
-
hsl(var(--hue-primary), var(--saturation-bg), var(--lightness-bg0))
|
|
8
|
-
);
|
|
8
|
+
background-color: var(--color-page-bg);
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
body {
|
|
@@ -44,11 +44,6 @@ footer a {
|
|
|
44
44
|
color: var(--color-text);
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
input[type="submit"]:not(.small) {
|
|
48
|
-
font-size: large;
|
|
49
|
-
padding: 0.25rem;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
47
|
input[type="search"],
|
|
53
48
|
input[type="text"] {
|
|
54
49
|
margin: 0.25rem 0;
|
|
@@ -71,18 +66,26 @@ input[type="text"] {
|
|
|
71
66
|
|
|
72
67
|
input[type="search"],
|
|
73
68
|
input[type="text"] {
|
|
74
|
-
color:
|
|
75
|
-
background-color:
|
|
76
|
-
var(--hue-primary),
|
|
77
|
-
var(--saturation-bg),
|
|
78
|
-
var(--lightness-bg2)
|
|
79
|
-
);
|
|
69
|
+
color: var(--color-text);
|
|
70
|
+
background-color: var(--color-surface);
|
|
80
71
|
border: none;
|
|
81
|
-
box-shadow: inset
|
|
72
|
+
box-shadow: inset 0 1px 3px var(--color-shadow);
|
|
73
|
+
transition: filter 0.15s;
|
|
74
|
+
|
|
75
|
+
&:not(:read-only):hover {
|
|
76
|
+
filter: brightness(1.05);
|
|
77
|
+
}
|
|
82
78
|
}
|
|
83
79
|
|
|
84
80
|
hr {
|
|
81
|
+
width: 100%;
|
|
85
82
|
margin: 1.5rem 0;
|
|
83
|
+
border: none;
|
|
84
|
+
border-top: 1px solid var(--color-track);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
::selection {
|
|
88
|
+
background-color: hsl(var(--hue-primary), 90%, 50%, 0.3);
|
|
86
89
|
}
|
|
87
90
|
|
|
88
91
|
code {
|
|
@@ -94,7 +97,7 @@ button {
|
|
|
94
97
|
cursor: pointer;
|
|
95
98
|
background-color: transparent;
|
|
96
99
|
border: none;
|
|
97
|
-
border-radius: 0.
|
|
100
|
+
border-radius: 0.5rem;
|
|
98
101
|
padding: 0.5rem;
|
|
99
102
|
min-width: 1rem;
|
|
100
103
|
|
|
@@ -103,19 +106,36 @@ button {
|
|
|
103
106
|
justify-content: center;
|
|
104
107
|
|
|
105
108
|
font-size: large;
|
|
109
|
+
transition: 0.15s;
|
|
106
110
|
|
|
107
|
-
&:hover
|
|
108
|
-
&:focus {
|
|
111
|
+
&:hover {
|
|
109
112
|
background-color: var(--color-primary);
|
|
110
113
|
color: white;
|
|
111
114
|
}
|
|
112
115
|
|
|
113
|
-
|
|
116
|
+
/* Keyboard focus only: an offset ring, no fill, so a focused button and
|
|
117
|
+
a hovered neighbor stay clearly two separate things. */
|
|
118
|
+
&:focus-visible {
|
|
114
119
|
outline: 2px solid var(--color-primary);
|
|
120
|
+
outline-offset: 2px;
|
|
115
121
|
opacity: 1 !important;
|
|
116
122
|
}
|
|
117
123
|
}
|
|
118
124
|
|
|
125
|
+
.badge {
|
|
126
|
+
font-size: 0.75rem;
|
|
127
|
+
font-weight: bold;
|
|
128
|
+
padding: 0.125rem 0.5rem;
|
|
129
|
+
border-radius: 1rem;
|
|
130
|
+
background-color: var(--color-surface);
|
|
131
|
+
box-shadow: 0 1px 2px var(--color-shadow);
|
|
132
|
+
|
|
133
|
+
&.type {
|
|
134
|
+
background-color: var(--color-primary);
|
|
135
|
+
color: white;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
119
139
|
ul {
|
|
120
140
|
margin: 0;
|
|
121
141
|
padding: 0;
|
|
@@ -135,11 +155,60 @@ dialog {
|
|
|
135
155
|
border: none;
|
|
136
156
|
outline: none;
|
|
137
157
|
|
|
158
|
+
/* Zoom/fade the dialog in and out. Browsers without @starting-style or
|
|
159
|
+
allow-discrete simply show and hide it instantly. */
|
|
160
|
+
opacity: 0;
|
|
161
|
+
transform: scale(0.95);
|
|
162
|
+
transition: opacity 0.2s ease-out, transform 0.2s ease-out,
|
|
163
|
+
display 0.2s allow-discrete, overlay 0.2s allow-discrete;
|
|
164
|
+
|
|
165
|
+
&[open] {
|
|
166
|
+
opacity: 1;
|
|
167
|
+
transform: scale(1);
|
|
168
|
+
|
|
169
|
+
@starting-style {
|
|
170
|
+
opacity: 0;
|
|
171
|
+
transform: scale(0.95);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
138
175
|
&::backdrop {
|
|
176
|
+
background-color: rgba(0, 0, 0, 0);
|
|
177
|
+
transition: background-color 0.2s ease-out,
|
|
178
|
+
display 0.2s allow-discrete, overlay 0.2s allow-discrete;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
&[open]::backdrop {
|
|
139
182
|
background-color: rgba(0, 0, 0, 0.5);
|
|
183
|
+
|
|
184
|
+
@starting-style {
|
|
185
|
+
background-color: rgba(0, 0, 0, 0);
|
|
186
|
+
}
|
|
140
187
|
}
|
|
141
188
|
|
|
142
|
-
> .
|
|
143
|
-
|
|
189
|
+
> .details {
|
|
190
|
+
background-color: var(--color-page-bg);
|
|
191
|
+
border-radius: 0.75rem;
|
|
192
|
+
padding: 1.5rem;
|
|
193
|
+
max-width: 100%;
|
|
194
|
+
|
|
195
|
+
header {
|
|
196
|
+
margin-bottom: 1rem;
|
|
197
|
+
|
|
198
|
+
h2 {
|
|
199
|
+
display: flex;
|
|
200
|
+
align-items: center;
|
|
201
|
+
gap: 0.5rem;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
.badges {
|
|
205
|
+
margin-top: 0.5rem;
|
|
206
|
+
gap: 0.5rem;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
main {
|
|
211
|
+
gap: 0.5rem;
|
|
212
|
+
}
|
|
144
213
|
}
|
|
145
214
|
}
|