gdk-toogle 0.9.5 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 126d3207f78acad2e5a2af3959117efd93a9a6b210187afafb4b4b69e1db33b9
4
- data.tar.gz: 26b6411779359a408db3f218e3e9d71d5f33538277c9cf276183c8756c63ede8
3
+ metadata.gz: 5b7fb4c9b37ce9f5712cc5635ff39bea2b3745179ea554dbe4b941f2a9472cab
4
+ data.tar.gz: 57a6c432b93d5dc0e88c52f46c653ca26338638f99f866c57e56dc5540a21692
5
5
  SHA512:
6
- metadata.gz: 19cd5ab3ca2ecf53b0f4712ada95810c468a37e963d70cf726081dacf6797ce2cf63d9cc193c79d924a658e7d5effcb4042f1e5cc0f0210a11b4cc077a45b04a
7
- data.tar.gz: 9f88ab74e9ed65a6ccdc031378d59b3310b52f2cdd0a8703d58f837cf7c357f9f2679b754fb525c99c1e7d934e81ef10e1c775f627ecad8c13da108b6aece209
6
+ metadata.gz: da4f86f837651e78dc12c7c61e2563cb8e099ff5a6a454e00226728906427243649190743cc36b46bcac34967551542afe8da58f5f5a35e5c6fd834412d434e4
7
+ data.tar.gz: 807b565d41e6f131a0811fb7e4d980f1c5a0a5ad5a61c892e81f3c9e6d4c2c614fe269bfadf8087465467deca63d3297080366ad1827b4abd302a09c5fb30e21
data/README.md CHANGED
@@ -4,23 +4,76 @@ 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 won't work in any other Rails app.
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 availabe at http://gdk.test:3000/rails/features.
13
+ The UI is available at http://gdk.test:3000/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
- ### How to run tests
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
- This is the default rake task. Just run `rake` with no arguments.
25
+ ### How to run it standalone
20
26
 
21
- The system specs in `spec/system` are configured to use Firefox as headless browser.
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
- **Tip:** If you don't have Firefox installed on your machine, or if you are on Ubuntu and have Firefox installed only via Snap, you can switch to Chrome by changing the `driven_by` to `:selenium_chrome_headless` in `spec/rails_helper.rb`.
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`).
24
77
 
25
78
  ## Tech stack
26
79
 
data/Rakefile CHANGED
@@ -13,4 +13,7 @@ require "rspec/core/rake_task"
13
13
  desc "Run all specs in spec directory"
14
14
  RSpec::Core::RakeTask.new(:spec)
15
15
 
16
- task default: :spec
16
+ # Provides rake tasks `standard` and `standard:fix`
17
+ require "standard/rake"
18
+
19
+ task default: %i[spec standard]
@@ -1,10 +1,41 @@
1
1
  document.addEventListener("alpine:init", () => {
2
- Alpine.data("features", (indexUrl) => ({
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
- indexUrl,
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
- fetch(this.indexUrl, {
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.features = data;
46
+ this.definitions = data;
16
47
  })
17
- .catch((error) => console.error("Error fetching data:", error));
48
+ .catch(showFetchError);
49
+
50
+ if (shareName) {
51
+ Promise.all([featuresLoaded, definitionsLoaded]).then(() =>
52
+ this.openDetails(shareName)
53
+ );
54
+ }
18
55
  },
19
56
 
20
- toggleFeature(feature) {
21
- const newState = feature.state === "enabled" ? "disabled" : "enabled";
22
- fetch(`${this.indexUrl}/${feature.name}`, {
23
- method: "PUT",
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
- body: JSON.stringify({
29
- state: newState,
30
- }),
31
- }).then(() => {
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
- deleteFeature(featureName) {
39
- const csrfToken = document.head.querySelector(
40
- "meta[name=csrf-token]"
41
- )?.content;
178
+ toggleDefault(definition) {
179
+ const newState = definition.default_enabled ? "disabled" : "enabled";
42
180
 
43
- fetch(`${this.indexUrl}/${featureName}`, {
44
- method: "DELETE",
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
- "X-CSRF-Token": csrfToken,
191
+ "Content-Type": "application/json",
192
+ "X-CSRF-Token": csrfToken(),
48
193
  },
49
- }).then(() => {
50
- this.features = this.features.filter((f) => f.name !== featureName);
51
- });
194
+ body: JSON.stringify({ state }),
195
+ }).catch(showFetchError);
52
196
  },
53
- }));
54
197
 
55
- Alpine.data("toggle", (featureName, isChecked, indexUrl) => ({
56
- name: featureName,
57
- checked: isChecked,
58
- indexUrl,
59
-
60
- input: {
61
- ["@change"]() {
62
- const csrfToken = document.head.querySelector(
63
- "meta[name=csrf-token]"
64
- )?.content;
65
-
66
- fetch(`${this.indexUrl}/${this.name}`, {
67
- method: "PUT",
68
- headers: {
69
- "Content-Type": "application/json",
70
- "X-CSRF-Token": csrfToken,
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
- #notice {
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
- top: 0;
19
- left: 0;
20
- right: 0;
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: white;
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
  }
@@ -42,30 +49,37 @@
42
49
  }
43
50
  }
44
51
 
45
- input:checked + .handle {
52
+ input:checked + .handle:before {
46
53
  background-color: var(--color-primary);
54
+ transform: translateX(26px);
47
55
  }
48
56
 
49
- input:focus + .handle {
50
- outline: 4px solid var(--color-primary);
51
- }
52
-
53
- input:checked + .handle:before {
54
- -webkit-transform: translateX(26px);
55
- -ms-transform: translateX(26px);
56
- transform: translateX(26px);
57
+ input:checked + .handle:active:before {
58
+ transform: translateX(26px) scale(0.92);
57
59
  }
58
60
 
59
- input:disabled + .handle:before {
60
- background-color: #ddd;
61
- content: "N/A";
62
- text-align: center;
63
- color: white;
64
- font-size: x-small;
65
- line-height: 26px;
61
+ input:focus-visible + .handle {
62
+ outline: 2px solid var(--color-primary);
63
+ outline-offset: 2px;
66
64
  }
67
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. */
68
68
  input:disabled + .handle {
69
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
+ }
70
84
  }
71
85
  }
@@ -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: hsl(var(--hue-primary), var(--saturation-bg), var(--lightness-fg));
75
- background-color: hsl(
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 1px 1px 2px var(--color-shadow);
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.125rem;
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
- &:focus {
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
- > .card {
143
- margin: 0;
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
  }
@@ -26,14 +26,13 @@ body {
26
26
  display: flex;
27
27
  flex-direction: column;
28
28
  align-items: center;
29
- justify-content: center;
30
29
  gap: 3rem;
31
30
 
32
31
  padding: 0 1rem;
33
32
  flex-grow: 1;
34
33
 
35
34
  &.with-sticky-search {
36
- margin-top: 4rem;
35
+ margin-top: 2rem;
37
36
 
38
37
  .sticky-search {
39
38
  align-self: center;
@@ -60,6 +59,50 @@ body {
60
59
  }
61
60
  }
62
61
 
62
+ .flags {
63
+ width: min(100%, 80ch);
64
+
65
+ section {
66
+ margin-top: 3rem;
67
+
68
+ h3 {
69
+ font-size: 0.875rem;
70
+ font-weight: bold;
71
+ text-transform: uppercase;
72
+ letter-spacing: 0.08em;
73
+ color: var(--color-text-muted);
74
+ }
75
+
76
+ ul {
77
+ margin-top: 1rem;
78
+
79
+ > p {
80
+ color: var(--color-text-muted);
81
+ }
82
+ }
83
+ }
84
+
85
+ li.shine {
86
+ animation: shine-fade 1.2s ease-out;
87
+ border-radius: 2px;
88
+ }
89
+ }
90
+
91
+ @keyframes shine-fade {
92
+ from {
93
+ background-color: hsl(var(--hue-primary), 10%, 50%, 0.25);
94
+ }
95
+ to {
96
+ background-color: transparent;
97
+ }
98
+ }
99
+
100
+ .metadata {
101
+ display: grid;
102
+ grid-template-columns: auto 1fr;
103
+ gap: 0.5rem;
104
+ }
105
+
63
106
  li.feature-toggle {
64
107
  display: flex;
65
108
  flex-direction: column;
@@ -69,29 +112,15 @@ li.feature-toggle {
69
112
  .row {
70
113
  align-items: center;
71
114
  flex-grow: 1;
115
+ gap: 0.25rem;
72
116
 
73
117
  button {
74
118
  opacity: 0;
75
- transition: 0.2s;
76
119
  }
77
120
  }
78
121
 
79
- > .col {
80
- padding: 0.5rem;
81
- }
82
-
83
- .more-info {
84
- padding-left: 68px; /* toggle width */
85
- }
86
-
87
- .metadata {
88
- display: grid;
89
- grid-template-columns: auto 1fr;
90
- gap: 0.5rem;
91
- }
92
-
93
122
  &:hover {
94
- background-color: rgba(153, 153, 153, 0.1);
123
+ background-color: hsl(var(--hue-primary), 10%, 50%, 0.07);
95
124
 
96
125
  button {
97
126
  opacity: 1;
@@ -24,10 +24,6 @@
24
24
  flex-grow: 1;
25
25
  }
26
26
 
27
- .stretch {
28
- align-self: stretch;
29
- }
30
-
31
27
  .gap {
32
28
  gap: 1em;
33
29
  }
@@ -1,28 +1,59 @@
1
1
  html {
2
+ color-scheme: light;
3
+
2
4
  --hue-primary: 35;
3
5
  --saturation-bg: 5%;
4
- --lightness-bg0: 80%;
5
- --lightness-bg1: 88%;
6
- --lightness-bg2: 96%;
6
+ --lightness-bg0: 92%; /* page */
7
+ --lightness-bg2: 97%; /* raised surfaces: inputs, badges */
8
+ --lightness-track: 84%; /* toggle tracks, slightly sunken */
9
+ --lightness-thumb: 99%; /* toggle thumbs (off) */
10
+ --lightness-disabled: 88%; /* disabled toggles, washed out into the page */
7
11
  --lightness-fg: 10%;
8
- --lightness-shadow: 50%;
12
+ --lightness-muted: 42%;
13
+ --lightness-shadow: 65%;
9
14
 
10
15
  --color-primary: hsl(var(--hue-primary), 90%, 50%);
11
16
  --color-text: hsl(var(--hue-primary), 0%, var(--lightness-fg));
17
+ --color-text-muted: hsl(var(--hue-primary), 5%, var(--lightness-muted));
12
18
  --color-shadow: hsl(0, var(--saturation-bg), var(--lightness-shadow));
13
19
  --color-page-bg: hsl(
14
20
  var(--hue-primary),
15
21
  var(--saturation-bg),
16
22
  var(--lightness-bg0)
17
23
  );
24
+ --color-surface: hsl(
25
+ var(--hue-primary),
26
+ var(--saturation-bg),
27
+ var(--lightness-bg2)
28
+ );
29
+ --color-track: hsl(
30
+ var(--hue-primary),
31
+ var(--saturation-bg),
32
+ var(--lightness-track)
33
+ );
34
+ --color-thumb: hsl(
35
+ var(--hue-primary),
36
+ var(--saturation-bg),
37
+ var(--lightness-thumb)
38
+ );
39
+ --color-disabled: hsl(
40
+ var(--hue-primary),
41
+ var(--saturation-bg),
42
+ var(--lightness-disabled)
43
+ );
18
44
  }
19
45
 
20
46
  @media (prefers-color-scheme: dark) {
21
47
  html:not(.light-mode) {
48
+ color-scheme: dark;
49
+
22
50
  --lightness-bg0: 8%;
23
- --lightness-bg1: 16%;
24
- --lightness-bg2: 24%;
51
+ --lightness-bg2: 16%;
52
+ --lightness-track: 22%;
53
+ --lightness-thumb: 70%;
54
+ --lightness-disabled: 12%;
25
55
  --lightness-fg: 90%;
26
- --lightness-shadow: 4%;
56
+ --lightness-muted: 60%;
57
+ --lightness-shadow: 3%;
27
58
  }
28
59
  }
@@ -5,16 +5,13 @@ module Toogle
5
5
  # content security policy.
6
6
  content_security_policy false, if: -> { Rails.env.development? }
7
7
 
8
- # Prevent all browser caching. Otherwise a cached version of a page might show the wrong toggle state.
9
- # Also, it really doesn't matter on localhost :)
8
+ # Never cache: a stale page could show the wrong toggle state.
10
9
  before_action :set_cache_headers
11
10
 
12
11
  private
13
12
 
14
13
  def set_cache_headers
15
- response.headers["Cache-Control"] = "no-cache, no-store, max-age=0, must-revalidate"
16
- response.headers["Pragma"] = "no-cache"
17
- response.headers["Expires"] = "Fri, 01 Jan 1990 00:00:00 GMT"
14
+ response.headers["Cache-Control"] = "no-store"
18
15
  end
19
16
  end
20
17
  end
@@ -1,10 +1,7 @@
1
1
  module Toogle
2
2
  class DefinitionsController < ApplicationController
3
- # This controller is only used to fetch the long list of feature definitions async.
4
- layout false
5
-
6
3
  def index
7
- @definitions = Toogle::Definition.unchanged
4
+ render json: Toogle::Definition.all
8
5
  end
9
6
  end
10
7
  end
@@ -1,7 +1,7 @@
1
1
  module Toogle
2
2
  class FeaturesController < ApplicationController
3
3
  def index
4
- @features = Toogle::Feature.all
4
+ load_features
5
5
 
6
6
  respond_to do |format|
7
7
  format.html
@@ -12,10 +12,14 @@ module Toogle
12
12
  def show
13
13
  @definition = Toogle::Definition.find(params[:id])
14
14
  if @definition.nil?
15
- flash[:notice] = "No feature defintion with name \"#{params[:id]}\" found."
15
+ flash.now[:notice] = "No feature definition with name \"#{params[:id]}\" found."
16
+ end
17
+ load_features
18
+
19
+ respond_to do |format|
20
+ format.html { render "index" }
21
+ format.json { render json: @features }
16
22
  end
17
- index
18
- render "index"
19
23
  end
20
24
 
21
25
  def update
@@ -51,5 +55,11 @@ module Toogle
51
55
  end
52
56
  end
53
57
  end
58
+
59
+ private
60
+
61
+ def load_features
62
+ @features = Toogle::Feature.all
63
+ end
54
64
  end
55
65
  end
@@ -2,7 +2,8 @@
2
2
 
3
3
  module Toogle
4
4
  class Definition
5
- attr_accessor :name, :default_enabled
5
+ attr_reader :name, :default_enabled, :milestone, :type,
6
+ :feature_issue_url, :introduced_by_url, :rollout_issue_url
6
7
 
7
8
  def self.all
8
9
  ::Feature::Definition.definitions.map do |definition|
@@ -10,25 +11,25 @@ module Toogle
10
11
  name: definition[0].to_s,
11
12
  default_enabled: definition[1].default_enabled,
12
13
  milestone: definition[1].milestone,
14
+ type: definition[1].type,
15
+ feature_issue_url: definition[1].feature_issue_url,
13
16
  introduced_by_url: definition[1].introduced_by_url,
14
- rollout_issue_url: definition[1].rollout_issue_url,
17
+ rollout_issue_url: definition[1].rollout_issue_url
15
18
  )
16
19
  end
17
20
  end
18
21
 
19
- def self.unchanged
20
- changed_features = Toogle::Feature.all.map(&:name)
21
- all.reject { |definition| changed_features.include?(definition.name) }
22
- end
23
-
24
22
  def self.find(name)
25
23
  all.find { |definition| definition.name == name }
26
24
  end
27
25
 
28
- def initialize(name:, default_enabled:, milestone: nil, introduced_by_url: nil, rollout_issue_url: nil)
26
+ def initialize(name:, default_enabled:, milestone: nil, type: nil,
27
+ feature_issue_url: nil, introduced_by_url: nil, rollout_issue_url: nil)
29
28
  @name = name
30
29
  @default_enabled = default_enabled
31
30
  @milestone = milestone
31
+ @type = type
32
+ @feature_issue_url = feature_issue_url
32
33
  @introduced_by_url = introduced_by_url
33
34
  @rollout_issue_url = rollout_issue_url
34
35
  end
@@ -2,18 +2,18 @@
2
2
 
3
3
  module Toogle
4
4
  class Feature
5
- attr_accessor :name, :state, :definition
5
+ attr_reader :name, :state, :definition
6
6
 
7
7
  def self.all
8
- definitions = Definition.all
8
+ definitions_by_name = Definition.all.index_by(&:name)
9
9
 
10
10
  Flipper::Adapters::ActiveRecord.new(
11
11
  feature_class: ::Feature::FlipperFeature,
12
12
  gate_class: ::Feature::FlipperGate
13
13
  ).get_all.map do |feature_name, feature_values|
14
- feature_definition = Definition.find(feature_name)
14
+ feature_definition = definitions_by_name[feature_name]
15
15
  feature_state = if feature_definition
16
- feature_values[:boolean] ? :enabled: :disabled
16
+ feature_values[:boolean] ? :enabled : :disabled
17
17
  else
18
18
  # This usually happens when switching back from an unmerged feature
19
19
  # branch that introduces a new flag, or when a flag got deleted.
@@ -3,12 +3,15 @@
3
3
  %head
4
4
  %meta{:content => "text/html; charset=UTF-8", "http-equiv" => "Content-Type"}/
5
5
  %meta(name="viewport" content="width=device-width, initial-scale=1.0")
6
+ -# GitLab's masked_referrer_url helper (Snowplow) crashes on referrers
7
+ -# pointing into this engine, because it cannot regenerate engine routes
8
+ -# with the main app's URL helpers. So never send a referrer.
9
+ %meta(name="referrer" content="no-referrer")
6
10
  %title Feature flags
7
11
  = csrf_meta_tags
8
12
  = csp_meta_tag
9
13
  = stylesheet_link_tag "toogle/application", media: "all"
10
14
  = javascript_include_tag 'toogle/application'
11
- %script(defer src="https://cdn.jsdelivr.net/npm/@alpinejs/anchor@3.x.x/dist/cdn.min.js")
12
15
  %script(defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js")
13
16
 
14
17
  %body
@@ -34,14 +37,22 @@
34
37
  %path(d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z")
35
38
 
36
39
  %main.with-sticky-search
37
- = yield
38
40
  - if notice.present?
39
- #notice.d-flex.row.center.gap
41
+ .notice.d-flex.row.center.gap
40
42
  %svg(width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round")
41
43
  %circle(cx="12" cy="12" r="10")
42
44
  %line(x1="12" y1="16" x2="12" y2="12")
43
45
  %line(x1="12" y1="8" x2="12.01" y2="8")
44
46
  %span.grow= notice
47
+ -# Client-side errors, e.g. fetch failures when the server is gone.
48
+ .notice.d-flex.row.center.gap(x-data x-show="$store.flash.message" x-cloak)
49
+ %svg(width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round")
50
+ %circle(cx="12" cy="12" r="10")
51
+ %line(x1="12" y1="16" x2="12" y2="12")
52
+ %line(x1="12" y1="8" x2="12.01" y2="8")
53
+ %span.grow(x-text="$store.flash.message")
54
+
55
+ = yield
45
56
 
46
57
  %footer
47
58
  %a(href="https://gitlab.com/thutterer/toogle" target="_blank")
@@ -0,0 +1,7 @@
1
+ -# The ⋯ button opening the details dialog. `subject` is the Alpine
2
+ -# expression for the flag's name, e.g. "feature.name".
3
+ %button(title="Show details" x-on:click="openDetails(#{subject})")
4
+ %svg(xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round")
5
+ %circle(cx="12" cy="12" r="1")
6
+ %circle(cx="12" cy="5" r="1")
7
+ %circle(cx="12" cy="19" r="1")
@@ -1,76 +1,103 @@
1
- .sticky-search
2
- .d-flex.col{
3
- "x-data": "{query: '', show: false}",
4
- "@click.outside": "show = false",
5
- "@keyup.esc": "show = false"}
1
+ .flags(x-data="flags('#{features_url}', '#{definitions_path}', '#{@definition&.name}')")
2
+ .sticky-search
6
3
  %input.large.w-100#search{
7
4
  "type": "search",
8
- "placeholder": "Search and toggle feature flags",
9
- "x-model.debounce.400ms": "query",
10
- "x-ref": "search",
11
- "x-on:input": "show = true",
12
- "x-on:focus": "show = true"}
13
- %template(x-if="show")
14
- .d-flex.col.stretch{
15
- "x-anchor.bottom-start.offset.5" => "$refs.search",
16
- "x-transition": nil}
17
- .grow{
18
- "data-url": definitions_path,
19
- "x-data": "{}",
20
- "x-init": "$el.innerHTML = await (await fetch($el.dataset.url)).text()"}
5
+ "placeholder": "Search feature flags",
6
+ "x-model.debounce.400ms": "query"}
21
7
 
22
- .card
23
- %h3 My feature flags
24
- %small These features already use custom settings in this GDK.
8
+ %section
9
+ %h3 Customized in this GDK
25
10
 
26
- %ul(x-data="features('#{features_url}')")
27
- %template(x-for="feature in features" :key="feature.name")
28
- %li.feature-toggle(x-data="{ showMore: false }"){"@click.outside": "showMore = false"}
29
- .d-flex.row.nowrap
30
- %template(x-if="feature.state === 'unknown'")
31
- %label.toggle(title="This feature does not exist on the current branch.")
32
- %input(type="checkbox" disabled :name="feature.name")
33
- %span.handle.round
34
- %template(x-if="feature.state !== 'unknown'")
11
+ %ul.pinned
12
+ %template(x-for="feature in filteredFeatures()" :key="feature.name")
13
+ %li.feature-toggle{":style": "feature.name === moving ? `view-transition-name: flag-${feature.name}` : ''", ":class": "{shine: feature.name === lastMoved}"}
14
+ .d-flex.row.nowrap
15
+ %template(x-if="feature.state === 'unknown'")
16
+ %label.toggle(title="This feature does not exist on the current branch.")
17
+ %input(type="checkbox" disabled :name="feature.name")
18
+ %span.handle.round
19
+ %template(x-if="feature.state !== 'unknown'")
20
+ %label.toggle
21
+ %input(type="checkbox" x-on:change="toggleFeature(feature)" :checked="feature.state === 'enabled'" :name="feature.name")
22
+ %span.handle.round
23
+
24
+ %code.grow.ellipsis(x-text="feature.name")
25
+
26
+ = render "details_button", subject: "feature.name"
27
+
28
+ %button(title="Forget this setting and use default" x-on:click="deleteFeature(feature.name)")
29
+ %svg(xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round")
30
+ %line(x1="18" y1="6" x2="6" y2="18")
31
+ %line(x1="6" y1="6" x2="18" y2="18")
32
+ %p(x-show="!features.length") Nothing custom yet. Toggle any flag below.
33
+ %p(x-show="features.length && !filteredFeatures().length") No matches.
34
+
35
+ %section
36
+ %h3 Defaults from the code
37
+
38
+ %ul.defaults
39
+ %template(x-for="definition in filteredDefinitions()" :key="definition.name")
40
+ %li.feature-toggle{":style": "definition.name === moving ? `view-transition-name: flag-${definition.name}` : ''", ":class": "{shine: definition.name === lastMoved}"}
41
+ .d-flex.row.nowrap
35
42
  %label.toggle
36
- %input(type="checkbox" x-on:change="toggleFeature(feature)" :checked="feature.state === 'enabled'" :name="feature.name")
37
- %span.handle.round
43
+ %input(type="checkbox" x-on:change="toggleDefault(definition)" :checked="definition.default_enabled" :name="definition.name")
44
+ %span.handle.round(:title="definition.default_enabled ? 'Enabled by default. Click to disable.' : 'Disabled by default. Click to enable.'")
45
+
46
+ %code.grow.ellipsis(x-text="definition.name")
38
47
 
39
- %code.grow.ellipsis(x-text="feature.name")
48
+ = render "details_button", subject: "definition.name"
49
+ %p(x-show="definitions.length && !matchingDefinitions().length")
50
+ No matches. Are you on the right branch?
51
+ %p(x-show="defaultsCapped()" x-text="`Showing first ${max} of ${matchingDefinitions().length}. Type to narrow down.`")
40
52
 
41
- %button(title="Show more" x-on:click="showMore = !showMore")
42
- %svg(xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round")
43
- %circle(cx="12" cy="12" r="1")
44
- %circle(cx="12" cy="5" r="1")
45
- %circle(cx="12" cy="19" r="1")
53
+ %dialog{"x-ref": "dialog", "@close": "closeDetails()", "@click.self": "$refs.dialog.close()"}
54
+ %template(x-if="selected()")
55
+ .details
56
+ %header
57
+ %h2
58
+ %template(x-if="selected().state === 'unknown'")
59
+ %label.toggle(title="This feature does not exist on the current branch.")
60
+ %input(type="checkbox" disabled)
61
+ %span.handle.round
62
+ %template(x-if="selected().state !== 'unknown'")
63
+ %label.toggle
64
+ %input(type="checkbox" x-on:change="toggleSelected()" :checked="selected().state === 'enabled'")
65
+ %span.handle.round
46
66
 
47
- %button(title="Forget this setting and use default" x-on:click="deleteFeature(feature.name)")
48
- %svg(xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round")
49
- %line(x1="18" y1="6" x2="6" y2="18")
50
- %line(x1="6" y1="6" x2="18" y2="18")
67
+ %code(x-text="selectedName")
51
68
 
52
- .d-flex.col.more-info{ "x-show": "showMore" }
53
- .metadata
54
- %strong Milestone
55
- %span(x-text="feature.definition.milestone || '-'")
56
- %strong Introduced by
57
- %a(x-show="feature.definition.introduced_by_url"
58
- x-text="`!${feature.definition.introduced_by_url?.split('/').at(-1)}`"
59
- :href="feature.definition.introduced_by_url"
60
- target="_blank")
61
- %span(x-show="!feature.definition.introduced_by_url") -
62
- %strong Rollout issue
63
- %a(x-show="feature.definition.rollout_issue_url"
64
- x-text="`#${feature.definition.rollout_issue_url?.split('/').at(-1)}`"
65
- :href="feature.definition.rollout_issue_url"
66
- target="_blank")
67
- %span(x-show="!feature.definition.rollout_issue_url") -
69
+ .d-flex.row.badges(x-show="selected().definition")
70
+ %span.badge.type{"x-show": "selected().definition?.type",
71
+ "x-text": "selected().definition?.type"}
72
+ %span.badge{"x-show": "selected().definition?.milestone",
73
+ "x-text": "`since ${selected().definition?.milestone}`"}
74
+
75
+ %main.d-flex.col
76
+ %template(x-if="selected().definition")
77
+ .metadata
78
+ %strong Feature issue
79
+ %a{"x-show": "selected().definition.feature_issue_url",
80
+ "x-text": "`#${selected().definition.feature_issue_url?.split('/').at(-1)}`",
81
+ ":href": "selected().definition.feature_issue_url",
82
+ "target": "_blank"}
83
+ %span(x-show="!selected().definition.feature_issue_url") -
84
+ %strong Introduced by
85
+ %a{"x-show": "selected().definition.introduced_by_url",
86
+ "x-text": "`!${selected().definition.introduced_by_url?.split('/').at(-1)}`",
87
+ ":href": "selected().definition.introduced_by_url",
88
+ "target": "_blank"}
89
+ %span(x-show="!selected().definition.introduced_by_url") -
90
+ %strong Rollout issue
91
+ %a{"x-show": "selected().definition.rollout_issue_url",
92
+ "x-text": "`#${selected().definition.rollout_issue_url?.split('/').at(-1)}`",
93
+ ":href": "selected().definition.rollout_issue_url",
94
+ "target": "_blank"}
95
+ %span(x-show="!selected().definition.rollout_issue_url") -
96
+ %template(x-if="!selected().definition")
97
+ %p This feature does not exist on the current branch.
68
98
 
69
99
  %hr
70
100
  %small Share this URL with MR reviewers.
71
101
  .d-flex.row
72
- %input.grow(x-ref="copyText" :value="`${indexUrl}${feature.name}`" type="text" readonly="readonly")
73
- %button(x-on:click="$refs.copyText.select(); document.execCommand('copy');") Copy
74
- %p(x-show="!features.length") No active feature flags in this GDK yet.
75
-
76
- = render "dialog"
102
+ %input.grow{":value": "`${featuresUrl}/${selectedName}`", "type": "text", "readonly": "readonly"}
103
+ %button(x-on:click="navigator.clipboard.writeText(`${featuresUrl}/${selectedName}`)") Copy
@@ -1,3 +1,3 @@
1
1
  module Toogle
2
- VERSION = "0.9.5"
2
+ VERSION = "1.0.1"
3
3
  end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gdk-toogle
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.5
4
+ version: 1.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Thomas Hutterer
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2024-05-29 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: rails
@@ -94,8 +93,6 @@ files:
94
93
  - app/assets/config/toogle_manifest.js
95
94
  - app/assets/javascript/toogle/application.js
96
95
  - app/assets/stylesheets/toogle/application.css
97
- - app/assets/stylesheets/toogle/components/card.css
98
- - app/assets/stylesheets/toogle/components/scrollbox.css
99
96
  - app/assets/stylesheets/toogle/components/toggle.css
100
97
  - app/assets/stylesheets/toogle/elements.css
101
98
  - app/assets/stylesheets/toogle/layout.css
@@ -107,9 +104,7 @@ files:
107
104
  - app/models/toogle/definition.rb
108
105
  - app/models/toogle/feature.rb
109
106
  - app/views/layouts/toogle/application.html.haml
110
- - app/views/toogle/definitions/index.html.haml
111
- - app/views/toogle/features/_dialog.html.haml
112
- - app/views/toogle/features/_toggle.html.haml
107
+ - app/views/toogle/features/_details_button.html.haml
113
108
  - app/views/toogle/features/index.html.haml
114
109
  - config/routes.rb
115
110
  - lib/toogle.rb
@@ -123,7 +118,6 @@ metadata:
123
118
  homepage_uri: https://gitlab.com/thutterer/toogle
124
119
  source_code_uri: https://gitlab.com/thutterer/toogle
125
120
  changelog_uri: https://gitlab.com/thutterer/toogle
126
- post_install_message:
127
121
  rdoc_options: []
128
122
  require_paths:
129
123
  - lib
@@ -138,8 +132,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
138
132
  - !ruby/object:Gem::Version
139
133
  version: '0'
140
134
  requirements: []
141
- rubygems_version: 3.4.10
142
- signing_key:
135
+ rubygems_version: 4.0.12
143
136
  specification_version: 4
144
137
  summary: Toogle toggles feature flags
145
138
  test_files: []
@@ -1,11 +0,0 @@
1
- .card {
2
- background-color: hsl(
3
- var(--hue-primary),
4
- var(--saturation-bg),
5
- var(--lightness-bg1)
6
- );
7
- box-shadow: 0 10px 16px var(--color-shadow);
8
- border-radius: 5px;
9
- padding: 1rem;
10
- max-width: 100%;
11
- }
@@ -1,26 +0,0 @@
1
- .scrollbox {
2
- max-height: 20ch;
3
- overflow: auto;
4
- padding: 1rem 0;
5
- word-break: break-all;
6
-
7
- background-color: hsl(
8
- var(--hue-primary),
9
- var(--saturation-bg),
10
- var(--lightness-bg2)
11
- );
12
- border: 2px solid
13
- hsl(var(--hue-primary), var(--saturation-bg), var(--lightness-bg1));
14
-
15
- border-style: ridge;
16
-
17
- box-shadow: 0 5px 10px var(--color-shadow);
18
-
19
- li {
20
- padding: 0 1rem;
21
-
22
- &:hover {
23
- background-color: hsl(var(--hue-primary), 60%, var(--lightness-bg1));
24
- }
25
- }
26
- }
@@ -1,9 +0,0 @@
1
- %ul.scrollbox
2
- - @definitions.each do |definition|
3
- - name = definition.name
4
- - enabled = definition.default_enabled
5
- %li.d-flex.center.row.nowrap(x-show="$el.textContent.includes(query) || query == ''")
6
- %label.toggle(x-data="toggle('#{name}', #{enabled}, '#{features_url}')")
7
- %input(type="checkbox" x-bind="input" x-model="checked"){checked: enabled}
8
- %span.handle.round(:title="checked ? 'Enabled by default. Click to disable.' : 'Disabled by default. Click to enable.'")
9
- %code.grow= name
@@ -1,18 +0,0 @@
1
- %dialog(x-ref="dialog" x-data="{enable: #{@definition.present?}, feature: '#{@definition&.name}', isAlreadyEnabled: #{@definition && @features.select {|f| f.state == :enabled }.map(&:name).include?(@definition.name) || false}}" x-init="if(enable) $refs.dialog.showModal()")
2
- .card{"x-on:click.outside": "$refs.dialog.close()"}
3
- %header
4
- %h2
5
- %label.toggle(x-data="toggle(feature, isAlreadyEnabled, '#{features_url}')")
6
- %input(type="checkbox" x-bind="input" x-model="checked")
7
- %span.handle.round
8
-
9
- %code(x-text="feature")
10
- %template(x-if="isAlreadyEnabled")
11
- %main
12
- %p
13
- This feature is already enabled.
14
- Click the toggle to disable it.
15
- %template(x-if="!isAlreadyEnabled")
16
- %main
17
- %p
18
- Click the toggle to enable this feature.
@@ -1,8 +0,0 @@
1
- - if feature.state == :unknown
2
- %label.toggle(title="This feature does not exist on the current branch.")
3
- %input(type="checkbox" disabled){name: feature.name}
4
- %span.handle.round
5
- - else
6
- %label.toggle(x-data="toggle('#{feature.name}', #{feature.state == :enabled})")
7
- %input(type="checkbox" x-bind="input" x-model="checked"){checked: feature.state == :enabled, name: feature.name}
8
- %span.handle.round(:title="checked ? 'Enabled. Click to disable.' : 'Disabled. Click to enable.'")