gdk-toogle 0.9.5 → 1.0.2
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 +61 -7
- data/Rakefile +4 -1
- data/app/assets/javascript/toogle/application.js +186 -54
- data/app/assets/stylesheets/toogle/application.css +1 -3
- data/app/assets/stylesheets/toogle/components/toggle.css +37 -20
- data/app/assets/stylesheets/toogle/elements.css +87 -18
- data/app/assets/stylesheets/toogle/layout.css +47 -18
- data/app/assets/stylesheets/toogle/utilities.css +0 -4
- data/app/assets/stylesheets/toogle/variables.css +61 -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 +16 -4
- data/app/views/toogle/features/_details_button.html.haml +7 -0
- data/app/views/toogle/features/index.html.haml +90 -63
- data/lib/toogle/version.rb +1 -1
- metadata +4 -11
- data/app/assets/stylesheets/toogle/components/card.css +0 -11
- 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: cacce942effef205bff4a9e31cd15889c6e3faba254dd3efee2e238519940147
|
|
4
|
+
data.tar.gz: f1e506fe72c8dbe264382fa96544a66e232ea0b40fb0bfb4fb14772b3a0e9702
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: aac129e17fb36ea58e72b2bd12dbf9d90b52d8055ab787146fc3fccaf0eec5cd178d6f1cf21672e0de6fd48aa9ab9ea15fa5a7d328e522ff430286ecf0c49d16
|
|
7
|
+
data.tar.gz: 83a9942634aab84f4058e743f900f538c5e5bc69c74f8c678c8738b544c96d377ef091cc96302164e33f03bc611db886cd90b10581b47b75a3b5e070ff32173f
|
data/README.md
CHANGED
|
@@ -4,23 +4,77 @@ 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
|
-
The UI is
|
|
13
|
+
The UI is available at http://gdk.test:4000/rails/features.
|
|
12
14
|
|
|
13
15
|
## Contributing
|
|
14
16
|
|
|
15
|
-
Everyone can contribute.
|
|
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.
|
|
16
20
|
|
|
17
|
-
|
|
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.
|
|
18
24
|
|
|
19
|
-
|
|
25
|
+
### How to run it standalone
|
|
20
26
|
|
|
21
|
-
The
|
|
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:
|
|
22
30
|
|
|
23
|
-
|
|
31
|
+
```sh
|
|
32
|
+
bundle install
|
|
33
|
+
cd spec/dummy
|
|
34
|
+
bin/rails server
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Then open http://localhost:4000/toogle. Toggled state lives in memory and
|
|
38
|
+
resets on restart.
|
|
39
|
+
|
|
40
|
+
To test your changes inside a real GDK, point GitLab's `Gemfile` at your
|
|
41
|
+
local checkout:
|
|
42
|
+
|
|
43
|
+
```ruby
|
|
44
|
+
gem 'gdk-toogle', path: '~/Code/toogle', require: 'toogle'
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### How to run tests and linting
|
|
48
|
+
|
|
49
|
+
Run `rake` to run both the specs and the [standard](https://github.com/standardrb/standard)
|
|
50
|
+
linter. They are also available separately as `rake spec` and `rake standard`
|
|
51
|
+
(use `rake standard:fix` to autoformat).
|
|
52
|
+
|
|
53
|
+
To lint automatically before each commit, enable the bundled git hook once:
|
|
54
|
+
|
|
55
|
+
```sh
|
|
56
|
+
git config core.hooksPath .githooks
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
The system specs in `spec/system` use headless Firefox by default. Set
|
|
60
|
+
`CAPYBARA_DRIVER` to any registered Capybara driver to switch, e.g. if you
|
|
61
|
+
don't have Firefox or only a Snap-installed one (incompatible with
|
|
62
|
+
geckodriver):
|
|
63
|
+
|
|
64
|
+
```sh
|
|
65
|
+
CAPYBARA_DRIVER=selenium_chrome_headless bundle exec rspec
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### How to cut a release
|
|
69
|
+
|
|
70
|
+
1. Bump `Toogle::VERSION` in `lib/toogle/version.rb`.
|
|
71
|
+
2. Add an entry to `CHANGELOG.md`.
|
|
72
|
+
3. Commit, then tag: `git tag vX.Y.Z && git push --tags`.
|
|
73
|
+
4. Build and push the gem: `rake build` then `gem push pkg/gdk-toogle-X.Y.Z.gem`.
|
|
74
|
+
5. Bump the version constraint in GitLab's `Gemfile` to pick it up.
|
|
75
|
+
|
|
76
|
+
Keep Ruby and gem versions at or below what GitLab itself runs (see
|
|
77
|
+
`AGENTS.md`).
|
|
24
78
|
|
|
25
79
|
## Tech stack
|
|
26
80
|
|
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
|
|
|
@@ -103,11 +238,8 @@ document.addEventListener("alpine:init", () => {
|
|
|
103
238
|
["@click"]() {
|
|
104
239
|
this.isDark = !this.isDark;
|
|
105
240
|
document.cookie = `toogle-dark=${this.isDark}; SameSite=Strict`;
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
} else {
|
|
109
|
-
document.documentElement.classList.add("light-mode");
|
|
110
|
-
}
|
|
241
|
+
document.documentElement.classList.toggle("dark-mode", this.isDark);
|
|
242
|
+
document.documentElement.classList.toggle("light-mode", !this.isDark);
|
|
111
243
|
},
|
|
112
244
|
["x-bind:title"]() {
|
|
113
245
|
return `Switch to ${this.isDark ? "Light Mode" : "Dark Mode"}`;
|
|
@@ -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;
|
|
@@ -15,11 +15,9 @@
|
|
|
15
15
|
.handle {
|
|
16
16
|
position: absolute;
|
|
17
17
|
cursor: pointer;
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
bottom: 0;
|
|
22
|
-
background-color: #ccc;
|
|
18
|
+
inset: 0;
|
|
19
|
+
background-color: var(--color-track);
|
|
20
|
+
box-shadow: inset 0 1px 3px var(--color-shadow);
|
|
23
21
|
transition: 0.2s;
|
|
24
22
|
|
|
25
23
|
&:before {
|
|
@@ -29,10 +27,19 @@
|
|
|
29
27
|
width: 26px;
|
|
30
28
|
left: 4px;
|
|
31
29
|
bottom: 4px;
|
|
32
|
-
background-color:
|
|
30
|
+
background-color: var(--color-thumb);
|
|
31
|
+
box-shadow: 0 1px 2px var(--color-shadow);
|
|
33
32
|
transition: 0.2s;
|
|
34
33
|
}
|
|
35
34
|
|
|
35
|
+
&:hover:before {
|
|
36
|
+
filter: brightness(1.1);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
&:active:before {
|
|
40
|
+
transform: scale(0.92);
|
|
41
|
+
}
|
|
42
|
+
|
|
36
43
|
&.round {
|
|
37
44
|
border-radius: 34px;
|
|
38
45
|
}
|
|
@@ -43,29 +50,39 @@
|
|
|
43
50
|
}
|
|
44
51
|
|
|
45
52
|
input:checked + .handle {
|
|
46
|
-
background-color: var(--color-
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
input:focus + .handle {
|
|
50
|
-
outline: 4px solid var(--color-primary);
|
|
53
|
+
background-color: var(--color-on);
|
|
51
54
|
}
|
|
52
55
|
|
|
53
56
|
input:checked + .handle:before {
|
|
54
|
-
-webkit-transform: translateX(26px);
|
|
55
|
-
-ms-transform: translateX(26px);
|
|
56
57
|
transform: translateX(26px);
|
|
57
58
|
}
|
|
58
59
|
|
|
59
|
-
input:
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
60
|
+
input:checked + .handle:active:before {
|
|
61
|
+
transform: translateX(26px) scale(0.92);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
input:focus-visible + .handle {
|
|
65
|
+
outline: 2px solid var(--color-primary);
|
|
66
|
+
outline-offset: 2px;
|
|
66
67
|
}
|
|
67
68
|
|
|
69
|
+
/* Truly disabled (e.g. flag unknown on this branch): flat and washed out
|
|
70
|
+
into the page, nothing raised or sunken that could look clickable. */
|
|
68
71
|
input:disabled + .handle {
|
|
69
72
|
cursor: not-allowed;
|
|
73
|
+
background-color: var(--color-disabled);
|
|
74
|
+
box-shadow: none;
|
|
75
|
+
|
|
76
|
+
&:before,
|
|
77
|
+
&:hover:before {
|
|
78
|
+
content: "N/A";
|
|
79
|
+
text-align: center;
|
|
80
|
+
color: var(--color-text-muted);
|
|
81
|
+
font-size: x-small;
|
|
82
|
+
line-height: 26px;
|
|
83
|
+
background-color: transparent;
|
|
84
|
+
box-shadow: none;
|
|
85
|
+
filter: none;
|
|
86
|
+
}
|
|
70
87
|
}
|
|
71
88
|
}
|
|
@@ -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
|
}
|