fuik 0.11.0 → 0.13.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.
- checksums.yaml +4 -4
- data/README.md +162 -25
- data/app/assets/images/fuik/icons/46elks.jpg +0 -0
- data/app/assets/images/fuik/icons/lettermint.jpg +0 -0
- data/app/assets/images/fuik/icons/smtp2go.jpg +0 -0
- data/app/assets/javascript/attractivejs-0.12.0.min.js +1 -0
- data/app/assets/stylesheets/fuik/application.css +204 -81
- data/app/controllers/concerns/fuik/event_type.rb +32 -7
- data/app/controllers/fuik/events_controller.rb +7 -2
- data/app/controllers/fuik/retries_controller.rb +12 -0
- data/app/controllers/fuik/webhooks_controller.rb +29 -8
- data/app/jobs/fuik/webhook_processing_job.rb +21 -0
- data/app/models/fuik/event.rb +36 -0
- data/app/models/fuik/webhook_event/filterable.rb +25 -0
- data/app/models/fuik/webhook_event.rb +34 -1
- data/app/views/fuik/events/_filters.html.erb +29 -0
- data/app/views/fuik/events/index.html.erb +7 -1
- data/app/views/fuik/events/show.html.erb +43 -20
- data/app/views/layouts/fuik/application.html.erb +12 -2
- data/config/routes.rb +4 -1
- data/lib/fuik/configuration.rb +29 -0
- data/lib/fuik/engine.rb +0 -4
- data/lib/fuik/errors.rb +5 -0
- data/lib/fuik/notifications.rb +48 -0
- data/lib/fuik/routing/provider_constraint.rb +4 -4
- data/lib/fuik/test_helpers.rb +33 -0
- data/lib/fuik/version.rb +1 -1
- data/lib/fuik.rb +9 -0
- data/lib/generators/fuik/provider/templates/base.rb.tt +11 -2
- data/lib/generators/fuik/provider/templates/github/base.rb.tt +3 -0
- data/lib/generators/fuik/provider/templates/lettermint/base.rb.tt +32 -0
- data/lib/generators/fuik/provider/templates/lettermint/message_failed.rb.tt +16 -0
- data/lib/generators/fuik/provider/templates/lettermint/message_hard_bounced.rb.tt +16 -0
- data/lib/generators/fuik/provider/templates/lettermint/message_soft_bounced.rb.tt +16 -0
- data/lib/generators/fuik/provider/templates/lettermint/message_suppressed.rb.tt +15 -0
- data/lib/generators/fuik/provider/templates/loops/base.rb.tt +2 -0
- data/lib/generators/fuik/provider/templates/postmark/base.rb.tt +1 -0
- data/lib/generators/fuik/provider/templates/smtp2go/base.rb.tt +4 -0
- data/lib/generators/fuik/provider/templates/smtp2go/bounce.rb.tt +16 -0
- data/lib/generators/fuik/provider/templates/smtp2go/reject.rb.tt +14 -0
- data/lib/generators/fuik/provider/templates/smtp2go/spam.rb.tt +13 -0
- data/lib/generators/fuik/provider/templates/smtp2go/unsubscribe.rb.tt +13 -0
- data/lib/generators/fuik/provider/templates/spinal/base.rb.tt +16 -0
- data/lib/generators/fuik/provider/templates/spinal/collection_synced.rb.tt +14 -0
- data/lib/generators/fuik/provider/templates/spinal/resource_created.rb.tt +14 -0
- data/lib/generators/fuik/provider/templates/spinal/resource_pushed.rb.tt +13 -0
- data/lib/generators/fuik/provider/templates/spinal/resource_scheduled.rb.tt +13 -0
- data/lib/generators/fuik/provider/templates/spinal/resource_unscheduled.rb.tt +12 -0
- metadata +32 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7e63ae360a189d38134db206274a5733ad73ea37ae2b338686c952df7ac3c109
|
|
4
|
+
data.tar.gz: c59410136f9c4143690b96b496c3e83fdf0419d806cc308a5ab4e73c1c1b21fb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 07d53ebe3d570ec39d8e2b669492433972789437100dd811b795e5abad902e5d921fc93cf6714c3fef5f5a484cbcdedab04280becea9ec02fb2e65e762517beb
|
|
7
|
+
data.tar.gz: 604fb6bc76e23853ca980b1f25861eab8ee45bfd2f0d66182d805db8b133fe411d35e485b44042937114236a040e5deb9563f248a703f245cedcc86d58b40cdc
|
data/README.md
CHANGED
|
@@ -6,7 +6,6 @@ Fuik (Dutch for fish trap) is a Rails engine that catches and stores webhooks fr
|
|
|
6
6
|
|
|
7
7
|
<img alt="Fuik admin interface" src="https://raw.githubusercontent.com/Rails-Designer/fuik/HEAD/.github/docs/webhooks-index.jpg" style="max-width: 100%;">
|
|
8
8
|
|
|
9
|
-
|
|
10
9
|
**Sponsored By [Rails Designer](https://railsdesigner.com/)**
|
|
11
10
|
|
|
12
11
|
<a href="https://railsdesigner.com/" target="_blank">
|
|
@@ -58,7 +57,17 @@ Visit `/webhooks` to see all received webhooks. Click any event to view all the
|
|
|
58
57
|
|
|
59
58
|
<img alt="Fuik event detail interface" src="https://raw.githubusercontent.com/Rails-Designer/fuik/HEAD/.github/docs/event-detail.jpg" style="max-width: 100%;">
|
|
60
59
|
|
|
61
|
-
⚠️ The `/webhooks` path is by default not protected.
|
|
60
|
+
⚠️ The `/webhooks` path is by default not protected. Set `Fuik.configuration.events_controller_parent` to a controller that requires authentication.
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
### Dashboard features
|
|
64
|
+
|
|
65
|
+
* **Copy payload as JSON**: click a button, payload is in your clipboard
|
|
66
|
+
* **Download payload as JSON file**: keep it for testing, debugging or throw it at your LLM agent, bot or colleague
|
|
67
|
+
* **Add `.json` to any URL**: get the raw payload without the UI
|
|
68
|
+
* **`GET /events.json`**: index as JSON with filter support
|
|
69
|
+
* **Click any key to get the Ruby accessor path**: click any key and get, e.g. `payload["line_items"][0]["product_id"]` (say what? 🤯)
|
|
70
|
+
|
|
62
71
|
|
|
63
72
|
|
|
64
73
|
### Add business logic
|
|
@@ -124,64 +133,192 @@ end
|
|
|
124
133
|
If `Provider::Base.verify!` exists, Fuik calls it automatically. Invalid signatures return 401 without storing the webhook.
|
|
125
134
|
|
|
126
135
|
|
|
136
|
+
### Configuration
|
|
137
|
+
|
|
138
|
+
Configure Fuik in `config/initializers/fuik.rb`:
|
|
139
|
+
```ruby
|
|
140
|
+
Fuik.configure do |config|
|
|
141
|
+
# config.events_controller_parent = "AdministrationController"
|
|
142
|
+
# config.providers_allowed = %w[stripe github postmark]
|
|
143
|
+
end
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
### Custom job class
|
|
148
|
+
|
|
149
|
+
Webhook events are processed asynchronously by `Fuik::WebhookProcessingJob`. When processing raises, the event is marked as `failed` so you can retry it from the dashboard.
|
|
150
|
+
|
|
151
|
+
Define a custom job class, then configure Fuik to use it:
|
|
152
|
+
```ruby
|
|
153
|
+
# app/jobs/webhook_processing_job.rb
|
|
154
|
+
class WebhookProcessingJob < ApplicationJob
|
|
155
|
+
queue_as :webhooks
|
|
156
|
+
|
|
157
|
+
retry_on MyApp::WebhookError, attempts: 5
|
|
158
|
+
discard_on ActiveJob::DeserializationError
|
|
159
|
+
|
|
160
|
+
def perform(event_class_name, webhook_event)
|
|
161
|
+
event_class_name.safe_constantize.new(webhook_event).process!
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
```ruby
|
|
167
|
+
# config/initializers/fuik.rb
|
|
168
|
+
Fuik.configure do |config|
|
|
169
|
+
config.webhook_processing_job_class = "WebhookProcessingJob"
|
|
170
|
+
end
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
The `perform` method receives two arguments:
|
|
174
|
+
- `event_class_name`: a string like `"Stripe::CheckoutSessionCompleted"` that resolves to your event class
|
|
175
|
+
- `webhook_event`: the `Fuik::WebhookEvent` record with the payload, headers and status
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
### Testing event classes
|
|
179
|
+
|
|
180
|
+
Unit test your `process!` methods without a database using the built-in test helper:
|
|
181
|
+
```ruby
|
|
182
|
+
# test/test_helper.rb
|
|
183
|
+
require "fuik/test_helpers"
|
|
184
|
+
|
|
185
|
+
class ActiveSupport::TestCase
|
|
186
|
+
include Fuik::TestHelpers
|
|
187
|
+
end
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
```ruby
|
|
191
|
+
# test/webhooks/stripe/checkout_session_completed_test.rb
|
|
192
|
+
module Stripe
|
|
193
|
+
class CheckoutSessionCompletedTest < ActiveSupport::TestCase
|
|
194
|
+
test "processes a completed checkout" do
|
|
195
|
+
event = build_webhook_event(
|
|
196
|
+
provider: "stripe",
|
|
197
|
+
event_type: "checkout.session.completed",
|
|
198
|
+
payload: { "client_reference_id" => "user_123" }
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
CheckoutSessionCompleted.new(event).process!
|
|
202
|
+
|
|
203
|
+
assert_equal "processed", event.status
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
test "fails when customer is not found" do
|
|
207
|
+
event = build_webhook_event(
|
|
208
|
+
provider: "stripe",
|
|
209
|
+
event_type: "checkout.session.completed",
|
|
210
|
+
payload: {}
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
CheckoutSessionCompleted.new(event).process!
|
|
214
|
+
|
|
215
|
+
assert_equal "failed", event.status
|
|
216
|
+
assert_equal "Customer not found", event.error
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
Your `process!` calls `@webhook_event.processed!` on success and `@webhook_event.failed!(error)` on failure:
|
|
223
|
+
```ruby
|
|
224
|
+
module Stripe
|
|
225
|
+
class CheckoutSessionCompleted < Base
|
|
226
|
+
def process!
|
|
227
|
+
user = User.find_by(id: payload.client_reference_id)
|
|
228
|
+
|
|
229
|
+
if user.present?
|
|
230
|
+
user.activate_subscription!
|
|
231
|
+
|
|
232
|
+
@webhook_event.processed!
|
|
233
|
+
else
|
|
234
|
+
@webhook_event.failed!("Customer not found")
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
|
|
127
242
|
### Provider allowlist
|
|
128
243
|
|
|
129
244
|
By default:
|
|
130
245
|
- **Development/test**: all providers are allowed
|
|
131
246
|
- **Production/staging**: only providers in `app/webhooks/` are allowed
|
|
132
247
|
|
|
133
|
-
Configure with `Fuik
|
|
248
|
+
Configure with `Fuik.configuration.providers_allowed`:
|
|
134
249
|
```ruby
|
|
135
250
|
# Allow all (including production)
|
|
136
|
-
Fuik
|
|
251
|
+
Fuik.configuration.providers_allowed = :all
|
|
137
252
|
|
|
138
253
|
# Explicit allowlist (overrides directory scan)
|
|
139
|
-
Fuik
|
|
254
|
+
Fuik.configuration.providers_allowed = %w[stripe github postmark]
|
|
140
255
|
```
|
|
141
256
|
|
|
142
257
|
Unknown providers return `404 Not Found`.
|
|
143
258
|
|
|
144
259
|
|
|
145
|
-
### Pre-packaged providers
|
|
146
|
-
|
|
147
|
-
Fuik includes ready-to-use [templates for common providers](https://github.com/Rails-Designer/fuik/tree/main/lib/generators/fuik/provider/templates).
|
|
148
|
-
|
|
149
|
-
|
|
150
260
|
### Event type & ID lookup
|
|
151
261
|
|
|
152
262
|
Fuik automatically extracts event types and IDs from common locations:
|
|
153
263
|
|
|
154
264
|
**Event Type:**
|
|
155
|
-
1. provider
|
|
265
|
+
1. provider Base class (if configured);
|
|
156
266
|
2. common headers (`X-Github-Event`, `X-Event-Type`, etc.);
|
|
157
267
|
3. payload (`type`, `event`, `event_type`);
|
|
158
268
|
4. falls back to `"unknown"`.
|
|
159
269
|
|
|
160
270
|
**Event ID:**
|
|
161
|
-
1. provider
|
|
271
|
+
1. provider Base class (if configured);
|
|
162
272
|
2. common headers (`X-GitHub-Delivery`, `X-Event-Id`, etc.);
|
|
163
273
|
3. payload (`id`).
|
|
164
274
|
4. falls back to MD5 hash of request body.
|
|
165
275
|
|
|
166
276
|
|
|
167
|
-
#### Custom lookup
|
|
277
|
+
#### Custom lookup
|
|
168
278
|
|
|
169
|
-
|
|
170
|
-
```
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
279
|
+
Define `event_type` and/or `event_id` on your provider's Base class if Fuik can't find them on its own:
|
|
280
|
+
```ruby
|
|
281
|
+
module CustomProvider
|
|
282
|
+
class Base < Fuik::Event
|
|
283
|
+
event_type header: "X-Custom-Event"
|
|
284
|
+
event_id header: "X-Custom-Id"
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
```
|
|
174
288
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
289
|
+
You can also read from the payload body or use a static value:
|
|
290
|
+
```ruby
|
|
291
|
+
event_type payload: "event_type" # payload["event_type"]
|
|
292
|
+
event_type payload: "data.type" # nested via dot-notation (payload["data"]["type"])
|
|
293
|
+
event_type "always_this_value" # static/literal
|
|
294
|
+
|
|
295
|
+
event_id payload: "id"
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
### Pre-packaged providers
|
|
300
|
+
|
|
301
|
+
Fuik includes ready-to-use [templates for common providers](https://github.com/Rails-Designer/fuik/tree/main/lib/generators/fuik/provider/templates).
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
## Monitoring
|
|
305
|
+
|
|
306
|
+
Fuik publishes lifecycle events via `ActiveSupport::Notifications`. Subscribe to track webhook activity:
|
|
307
|
+
```ruby
|
|
308
|
+
ActiveSupport::Notifications.subscribe("webhook_received.fuik") do |event|
|
|
309
|
+
Rails.logger.info("[Fuik] Received #{event.payload[:provider]}:#{event.payload[:event_type]}")
|
|
310
|
+
end
|
|
178
311
|
```
|
|
179
312
|
|
|
180
|
-
|
|
313
|
+
Available events:
|
|
181
314
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
315
|
+
| Event | When it fires |
|
|
316
|
+
|---|---|
|
|
317
|
+
| `webhook_received.fuik` | Event record created |
|
|
318
|
+
| `webhook_processed.fuik` | `process!` completed |
|
|
319
|
+
| `webhook_failed.fuik` | `process!` raised |
|
|
320
|
+
| `webhook_signature_invalid.fuik` | Signature verification failed (401) |
|
|
321
|
+
| `webhook_receive_error.fuik` | Unexpected error during receive (500) |
|
|
185
322
|
|
|
186
323
|
|
|
187
324
|
## Add your custom provider
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).Attractive=e()}(this,(function(){"use strict";class t{static enabled=!1;static prefix="🧲 ";static log(...t){this.enabled&&console.log(this.prefix,...t)}static warn(...t){this.enabled&&console.warn(this.prefix,...t)}static error(...t){this.enabled&&console.error(this.prefix,...t)}static throw(t){throw new Error(`${this.prefix}${t}`)}}class e{#t;constructor(e){t.log("Events with actions:",Object.keys(e)),this.#t=e}process(t,{on:e,using:s}){e&&this.#e(e.dataset.action).some((r=>!1===this.#s(r,{for:t,on:e,using:s})))}#e(t){return t.split(" ").filter((t=>t))}#s(e,{for:s,on:r,using:i}){if(t.log("Process action for",s.type,"on",r,"…"),e.includes(":")){const[t,r]=e.split(":");if(s.type!==r)return;e=t}else if(e.includes("->")){const[t,r]=e.split("->");if((t.includes("@")?t.split("@")[1]:t)!==s.type)return;e=r}else if(s.type!==i)return;return t.log("…","processed action for",s.type,"on",r),this.#r(e,{on:r,for:s})}#r(e,{on:s,for:r}){t.log("Execute action",e,"on",s,"…");const i=e.split("#"),[n,a,o]=i,c=this.#t[n]?n:a??e;if("function"!=typeof this.#t[c])return;const l=this.#t[n]?i.slice(1).join("#"):o??null,u=this.#t[c](s,{value:l,target:s.dataset.target,targets:s.dataset.targets});return!1===u&&r&&r.preventDefault(),t.log("…","executed action",e,"on",s),u}}var s=new class{identify({by:t}){return t.split(" ").filter((t=>t.includes("->"))).map((t=>t.split("->")[0]))}getDefault({from:t}){const e=t.tagName.toLowerCase(),s="input"===e,r=s?t.type||"text":null;return s?this.#i.input[r]||this.#i.input.default:this.#i[e]||this.#i.default}#i={a:"click",button:"click",input:{checkbox:"change",radio:"change",submit:"click",button:"click",reset:"click",default:"input"},select:"change",textarea:"input",form:"submit",default:"click"}};class r{#n;#a;#o;constructor(t,e=null){this.#n=t,this.#a=e}start(t){if(window.MutationObserver)return this.#o=new MutationObserver((e=>{const s=new Set,r=new Set;e.forEach((e=>this.#c(e,{for:t,elements:{added:s,removed:r}}))),s.forEach((t=>this.#n(t))),this.#a&&r.forEach((t=>this.#a(t)))})),this.#o.observe(document.documentElement,{childList:!0,subtree:!0}),this}stop(){return this.#o&&this.#o.disconnect(),this}#c(t,{for:e,elements:{added:s,removed:r}}){"childList"===t.type&&(t.addedNodes.forEach((t=>{this.#l(t,{for:e,and:s})})),t.removedNodes.forEach((t=>{this.#l(t,{for:e,and:r})})))}#l(t,{for:e,and:s}){t.nodeType===Node.ELEMENT_NODE&&(t.matches&&t.matches(e)&&s.add(t),t.querySelectorAll&&t.querySelectorAll(e).forEach((t=>s.add(t))))}}var i=new class{#u={mounted:(t,e)=>{e()},now:(t,e)=>{e()},whenVisible:(t,e)=>{const s=new IntersectionObserver((t=>{t.forEach((t=>{t.isIntersecting&&(e(),s.disconnect())}))}));s.observe(t)}};setup({for:t,on:e,trigger:s}){const r=this.#u[t];return!!r&&(r(e,s),!0)}};class n{constructor(t,e={}){if(!t)throw new Error("Current element is required");this.currentElement=t,this.target=e.target,this.targetsSelector=e.targets,this.options=e}get targets(){if(this.targetsSelector)return Array.from(document.querySelectorAll(this.targetsSelector));if(this.target){const t=document.getElementById(this.target);return t?[t]:[]}return[this.currentElement]}cycledValue(t,e){const s=Array.isArray(e)?e:e.split(",").map((t=>t.trim())),r=s.indexOf(t);return s[(r+1)%s.length]}}class a extends n{constructor(t,e={}){super(t,e);const[s,r]=e.value.split("=");this.attribute=s,this.value=r}toggle(){this.attribute&&this.targets.forEach((t=>t.hasAttribute(this.attribute)?t.removeAttribute(this.attribute):t.setAttribute(this.attribute,this.value||"")))}cycle(){this.value&&this.targets.forEach((t=>this.#h(t)))}add(){this.attribute&&this.targets.forEach((t=>t.setAttribute(this.attribute,this.value||"")))}remove(){this.attribute&&this.targets.forEach((t=>t.removeAttribute(this.attribute)))}#h(t){const e=this.cycledValue(t.getAttribute(this.attribute),this.value);t.setAttribute(this.attribute,e)}}const o=t=>(e,s={})=>new a(e,s)[t]();var c={toggleAttribute:o("toggle"),cycleAttribute:o("cycle"),addAttribute:o("add"),removeAttribute:o("remove")};const l=t=>t?.split(",").map((t=>t.trim())).filter(Boolean)??[];class u extends n{constructor(t,e={}){super(t,e),this.value=l(e.value)}toggle(){this.value&&this.targets.forEach((t=>this.#d({forEach:t})))}cycle(){this.value&&0!==this.value.length&&this.targets.forEach((t=>this.#f(t)))}add(){this.value&&this.targets.forEach((t=>t.classList.add(...this.value)))}remove(){this.value&&this.targets.forEach((t=>t.classList.remove(...this.value)))}#d({forEach:t}){this.value.forEach((e=>t.classList.toggle(e)))}#f(t){const e=this.value.find((e=>t.classList.contains(e)))||"",s=this.cycledValue(e,this.value);t.classList.remove(...this.value),t.classList.add(s)}}const h=t=>(e,s={})=>new u(e,s)[t]();var d={toggleClass:h("toggle"),cycleClass:h("cycle"),addClass:h("add"),removeClass:h("remove")};let f;const g=(t,e)=>{clearTimeout(f),f=setTimeout(t,e)};class v extends n{constructor(t,e={}){super(t,e),this.value=e.value}async copy(){const t=this.value||(this.targets[0]?.value??this.targets[0]?.textContent);if(void 0!==t)try{await navigator.clipboard.writeText(t),this.#g(!0)}catch(t){this.#g(!1)}}#g(t){const e=parseInt(this.currentElement.dataset.copyDelay);this.targets.forEach((e=>e.setAttribute(this.#v,t))),e&&g((()=>this.targets.forEach((t=>t.removeAttribute(this.#v)))),e)}get#v(){return"data-copy-success"}}var m,p={copy:(m="copy",(t,e={})=>new v(t,e)[m]())};class b extends n{confirm(){const t=this.currentElement.dataset.confirmMessage||"Are you sure?",e=window.confirm(t);return this.#g(e),e}#g(t){this.currentElement.setAttribute("data-confirm-success",t);const e=this.currentElement.dataset.confirmDuration;e&&g((()=>this.currentElement.removeAttribute("data-confirm-success")),parseInt(e))}}var E={confirm:(t=>(e,s={})=>new b(e,s)[t]())("confirm")};class w extends n{constructor(t,e={}){super(t,e);const[s,r]=e.value.split("=");this.attribute=s,this.value=r}toggle(){this.attribute&&this.targets.forEach((t=>{this.attribute in t.dataset?delete t.dataset[this.attribute]:t.dataset[this.attribute]=this.value||""}))}cycle(){this.value&&this.targets.forEach((t=>this.#m(t)))}add(){this.attribute&&this.targets.forEach((t=>{t.dataset[this.attribute]=this.value||""}))}remove(){this.attribute&&this.targets.forEach((t=>delete t.dataset[this.attribute]))}#m(t){const e=this.cycledValue(t.dataset[this.attribute],this.value);t.dataset[this.attribute]=e}}const y=t=>(e,s={})=>new w(e,s)[t]();var A={toggleDataAttribute:y("toggle"),cycleDataAttribute:y("cycle"),addDataAttribute:y("add"),removeDataAttribute:y("remove")};class L extends n{open(){this.targets.forEach((t=>t instanceof HTMLDialogElement&&t.show()))}openModal(){this.targets.forEach((t=>t instanceof HTMLDialogElement&&t.showModal()))}close(){this.targets.forEach((t=>t instanceof HTMLDialogElement&&t.close()))}}const T=t=>(e,s={})=>{new L(e,s)[t]()};var x={open:T("open"),openModal:T("openModal"),close:T("close")};class M extends n{add(){const t=this.targets[0];if(!t)return;const e=this.currentElement.dataset.addSource;if(!e)return;const s=document.querySelector(e);if(!s)return;const r=this.#p(s),i=this.currentElement.dataset.addAt||"beforeend";t.insertAdjacentElement(i,r)}remove(){const t=parseInt(this.currentElement.dataset.removeDelay),e=()=>{this.targets.forEach((t=>t.remove()))};t?setTimeout(e,t):e()}#p(t){return"TEMPLATE"===t.tagName?t.content.cloneNode(!0).firstElementChild:t.cloneNode(!0)}}const S=t=>(e,s={})=>new M(e,s)[t]();var k={add:S("add"),remove:S("remove")};class N extends n{requestSubmit(){const t=parseInt(this.currentElement.dataset.submitDelay)||0,e=()=>this.targets.forEach((t=>t instanceof HTMLFormElement&&t.requestSubmit()));t?g(e,t):e()}reset(){this.targets.forEach((t=>t instanceof HTMLFormElement&&t.reset()))}}const F=t=>(e,s={})=>{new N(e,s)[t]()};var D={submit:F("requestSubmit"),reset:F("reset")};class q extends n{constructor(t,e={}){super(t,e),this.classNames=l(e.value),this.justOnce="intersect-once"===e.actionName}start(){if(0===this.classNames.length)return;const t=new IntersectionObserver((e=>{e.forEach((e=>{e.isIntersecting?this.#b(t,e.target):this.#E()}))}));t.observe(this.currentElement)}#b(t,e){this.targets.forEach((t=>t.classList.add(...this.classNames))),this.justOnce&&t.unobserve(e)}#E(){this.justOnce||this.targets.forEach((t=>t.classList.remove(...this.classNames)))}}const C=t=>(e,s={})=>new q(e,{...s,actionName:t}).start();var O={"intersect-once":C("intersect-once"),"intersect-toggle":C("intersect-toggle")};class I extends n{reload(){this.targets.forEach((t=>{this.#w(t)?t.reload():window.location.reload()}))}#w(t){return"TURBO-FRAME"===t.tagName&&"function"==typeof t.reload}}const H=t=>(e,s={})=>new I(e,s)[t]();var V={reload:H("reload"),refresh:H("reload")};class j{static get(){const t=document.querySelector('meta[name="csrf-token"]')?.content;return t||null}}class $ extends n{constructor(t,e={}){super(t,e),this.value=e.value}async get(){if(this.value){this.#y({with:this.value})&&console.warn(`Cross-origin request to: ${this.value}. Missing the correct CORS headers.`),this.#g("busy");try{const t=await fetch(this.value,{method:"GET"});if(!t.ok)throw new Error(`HTTP error! status: ${t.status}`);const e=await t.text();return this.targets.forEach((t=>{t.innerHTML=e})),this.#g("success"),t}catch(t){throw console.error("GET request failed:",t),this.#g("error"),t}}else console.warn("No URL provided in the action value")}post(){return this.#A("POST")}patch(){return this.#A("PATCH")}put(){return this.#A("PUT")}#y({with:t}){try{return new URL(t,window.location.href).origin!==window.location.origin}catch{return console.error("Invalid URL:",t),!1}}#g(t){const e=this.currentElement.dataset.requestDuration;this.targets.forEach((e=>{"busy"===t?(e.setAttribute("data-request-busy","true"),e.removeAttribute("data-request-success")):(e.removeAttribute("data-request-busy"),e.setAttribute("data-request-success","success"===t))})),e&&"busy"!==t&&g((()=>{this.targets.forEach((t=>t.removeAttribute("data-request-success")))}),parseInt(e))}#A(t){if(this.value)return this.#g("busy"),fetch(this.value,{method:t,headers:{"Content-Type":"application/json","X-CSRF-Token":j.get()},body:JSON.stringify(this.#L)}).then((t=>{if(!t.ok)throw new Error(`HTTP error! status: ${t.status}`);return this.#g("success"),t})).catch((e=>{throw console.error(`${t} request failed:`,e),this.#g("error"),e}));console.warn("No URL provided in the action value")}get#L(){const t={},e=this.targets[0];if(e instanceof HTMLFormElement){const s=new FormData(e);for(const[e,r]of s.entries())t[e]=r;return t}if(this.#T){const e=this.currentElement.name,s=this.currentElement.value;t[e]=s}return t}get#T(){return this.currentElement instanceof HTMLInputElement||this.currentElement instanceof HTMLSelectElement||this.currentElement instanceof HTMLTextAreaElement&&this.currentElement.name}}const R=t=>(e,s={})=>new $(e,s)[t]();var P={get:R("get"),post:R("post"),patch:R("patch"),put:R("put")};class U extends n{constructor(t,e={}){super(t,e);const s=e.value;this.behavior=["auto","instant","smooth"].includes(s)?s:"auto"}scroll(){this.targets[0]?.scrollIntoView({behavior:this.behavior})}}var z={scrollTo:(t=>(e,s={})=>new U(e,s)[t]())("scroll")};const B={attribute:c,class:d,clipboard:p,confirm:E,dataAttribute:A,dialog:x,element:k,form:D,intersection:O,reload:V,request:P,scrollTo:z},G=(t=[])=>0===t.length?Object.values(B).reduce(((t,e)=>({...t,...e})),{}):B.reduce(((e,s)=>t[s]?{...e,...t[s]}:(console.warn(`Action “${s}” not found`),e)),{});var _=G([]);var J=new class{#x={};#M=new WeakMap;#S=new Map;#k;#N=new Map;#F;static get debug(){return t.enabled}static set debug(e){t.enabled=e}constructor(){this.#k=new e(_),this.#F=new r((t=>this.#n(t)),(t=>this.#a(t)))}activate(e={}){const{on:s=document,debug:r=!1}=e;return t.enabled=r,t.log("Initializing…"),this.element=s,this.#F.start("[data-action]"),this.element.querySelectorAll("[data-action]").forEach((t=>this.#n(t))),t.log("…initialized"),this}withActions(s=[]){return t.log("Initializing with actions",s),this.#k=new e(G(s)),this}#n(t){const e=t.dataset.action;if(!e)return;const r=e.split(" ");new Set(e.includes("->")?s.identify({by:e}):[s.getDefault({from:t})]).forEach((t=>this.#D({for:t}))),r.filter((t=>t.includes("@"))).forEach((e=>{const[s]=e.split("->"),[r,i]=s.split("@"),n="window"===r?window:document;this.#q(i,n,t)}));const n=r.filter((t=>t.includes(":"))).map((t=>t.split(":")[1]));n.forEach((e=>{i.setup({for:e,on:t,trigger:()=>this.#k.process({type:e},{on:t,using:e})})}))}#a(t){const e=this.#M.get(t);e&&(e.forEach((e=>{const s=this.#N.get(e);if(s&&(s.delete(t),0===s.size)){const[t,s]=e.split(":"),r=this.#S.get(e);("window"===t?window:document).removeEventListener(s,r),this.#S.delete(e),this.#N.delete(e)}})),this.#M.delete(t))}#D({for:e}){this.#x[e]||(this.element.addEventListener(e,(t=>this.#C(t))),t.log("Added event listener for",e,"to",this.element),this.#x[e]=!0)}#q(t,e,s){const r=`${e===window?"window":"document"}:${t}`;if(this.#M.has(s)||this.#M.set(s,new Set),this.#M.get(s).add(r),this.#N.has(r)||this.#N.set(r,new Set),this.#N.get(r).add(s),!this.#S.has(r)){const s=e=>{const s=this.#N.get(r);s&&s.forEach((s=>{this.#k.process(e,{on:s,using:t})}))};e.addEventListener(t,s),this.#S.set(r,s)}}#C(t){const e=t.target.closest("[data-action]");if(!e)return;const r=s.getDefault({from:e});this.#k.process(t,{on:e,using:r})}};const W=(t=document)=>{window.Attractive&&window.Attractive._initialized||J.activate({element:t})};return"undefined"!=typeof document&&("loading"===document.readyState?document.addEventListener("DOMContentLoaded",(()=>W())):W(),document.addEventListener("turbo:load",(()=>W()))),"undefined"!=typeof window&&(window.Attractive=J),J}));
|
|
@@ -2,10 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
@layer base {
|
|
4
4
|
:root {
|
|
5
|
-
--
|
|
6
|
-
--
|
|
7
|
-
--
|
|
8
|
-
--
|
|
5
|
+
--gray-50: oklch(98.7% .002 197.1);
|
|
6
|
+
--gray-100: oklch(96.3% .002 197.1);
|
|
7
|
+
--gray-200: oklch(92.5% .005 214.3);
|
|
8
|
+
--gray-300: oklch(87.2% .007 219.6);
|
|
9
|
+
--gray-400: oklch(72.3% .014 214.4);
|
|
10
|
+
--gray-500: oklch(56% .021 213.5);
|
|
11
|
+
--gray-600: oklch(45% .017 213.2);
|
|
12
|
+
--gray-700: oklch(37.8% .015 216);
|
|
13
|
+
--gray-800: oklch(27.5% .011 216.9);
|
|
14
|
+
--gray-900: oklch(21.8% .008 223.9);
|
|
15
|
+
--gray-950: oklch(14.8% .004 228.8);
|
|
9
16
|
}
|
|
10
17
|
|
|
11
18
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
@@ -13,7 +20,8 @@
|
|
|
13
20
|
body {
|
|
14
21
|
font-family: system-ui, -apple-system, sans-serif;
|
|
15
22
|
line-height: 1.5;
|
|
16
|
-
color: var(--
|
|
23
|
+
color: light-dark(var(--gray-800), var(--gray-200));
|
|
24
|
+
background-color: light-dark(white, var(--gray-950));
|
|
17
25
|
}
|
|
18
26
|
|
|
19
27
|
main {
|
|
@@ -22,19 +30,6 @@
|
|
|
22
30
|
margin-inline: auto;
|
|
23
31
|
}
|
|
24
32
|
|
|
25
|
-
h1 {
|
|
26
|
-
margin-block-end: 1rem;
|
|
27
|
-
font-size: 1.875rem;
|
|
28
|
-
font-weight: 700;
|
|
29
|
-
letter-spacing: -.025em;
|
|
30
|
-
|
|
31
|
-
a {
|
|
32
|
-
color: var(--color-text-muted);
|
|
33
|
-
|
|
34
|
-
&:hover { color: var(--color-text); }
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
33
|
a {
|
|
39
34
|
color: inherit;
|
|
40
35
|
text-decoration: none;
|
|
@@ -47,60 +42,38 @@
|
|
|
47
42
|
padding-block: .5rem;
|
|
48
43
|
padding-inline: 1rem;
|
|
49
44
|
max-height: calc(20lh + 1rem);
|
|
45
|
+
width: 100%;
|
|
50
46
|
font-size: .875rem;
|
|
51
|
-
background: var(--
|
|
52
|
-
border
|
|
47
|
+
background: light-dark(var(--gray-50), var(--gray-800));
|
|
48
|
+
border: 1px solid light-dark(transparent, var(--gray-700));
|
|
49
|
+
border-radius: .375rem;
|
|
53
50
|
overflow-x: auto;
|
|
54
51
|
}
|
|
52
|
+
}
|
|
55
53
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
font-weight: 600;
|
|
64
|
-
color: var(--color-text);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
dd {
|
|
68
|
-
position: relative;
|
|
69
|
-
overflow-x: auto;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
.actions {
|
|
73
|
-
display: flex;
|
|
74
|
-
align-items: center;
|
|
75
|
-
gap: .5rem;
|
|
76
|
-
position: absolute;
|
|
77
|
-
top: .5rem; right: .5rem;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
svg {
|
|
81
|
-
width: 1rem;
|
|
82
|
-
aspect-ratio: 1 / 1;
|
|
83
|
-
color: oklch(from var(--color-text) l c h / 80%);
|
|
84
|
-
background: var(--color-bg);
|
|
85
|
-
}
|
|
54
|
+
@layer components {
|
|
55
|
+
.heading {
|
|
56
|
+
margin-block-end: 1rem;
|
|
57
|
+
font-size: 1.875rem;
|
|
58
|
+
font-weight: 550;
|
|
59
|
+
letter-spacing: -.0125em;
|
|
60
|
+
color: light-dark(var(--gray-700), var(--gray-100));
|
|
86
61
|
|
|
87
|
-
|
|
88
|
-
|
|
62
|
+
.divider {
|
|
63
|
+
font-weight: 400;
|
|
64
|
+
color: light-dark(var(--gray-600), var(--gray-300));
|
|
89
65
|
}
|
|
90
66
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}
|
|
67
|
+
a {
|
|
68
|
+
color: light-dark(var(--gray-500), var(--gray-400));
|
|
94
69
|
|
|
95
|
-
|
|
96
|
-
display: block;
|
|
70
|
+
&:hover { color: light-dark(var(--gray-800), var(--gray-200)); }
|
|
97
71
|
}
|
|
98
72
|
}
|
|
99
|
-
}
|
|
100
73
|
|
|
101
|
-
@layer components {
|
|
102
74
|
.events {
|
|
103
75
|
list-style: none;
|
|
76
|
+
margin-block-start: 1rem;
|
|
104
77
|
|
|
105
78
|
li {
|
|
106
79
|
--icon-width: 1.125rem;
|
|
@@ -108,7 +81,7 @@
|
|
|
108
81
|
padding-block: .5rem;
|
|
109
82
|
padding-inline: .75rem;
|
|
110
83
|
|
|
111
|
-
&:hover { background: var(--
|
|
84
|
+
&:hover { background: light-dark(var(--gray-50), var(--gray-800)); }
|
|
112
85
|
|
|
113
86
|
a {
|
|
114
87
|
display: grid;
|
|
@@ -118,6 +91,12 @@
|
|
|
118
91
|
&:hover { text-decoration: none; }
|
|
119
92
|
}
|
|
120
93
|
|
|
94
|
+
.icon {
|
|
95
|
+
width: var(--icon-width);
|
|
96
|
+
aspect-ratio: 1/1;
|
|
97
|
+
border-radius: .15rem;
|
|
98
|
+
}
|
|
99
|
+
|
|
121
100
|
.name {
|
|
122
101
|
display: grid;
|
|
123
102
|
grid-template-columns: var(--icon-width) 1fr;
|
|
@@ -125,12 +104,6 @@
|
|
|
125
104
|
gap: .5rem;
|
|
126
105
|
}
|
|
127
106
|
|
|
128
|
-
.icon {
|
|
129
|
-
width: var(--icon-width);
|
|
130
|
-
aspect-ratio: 1/1;
|
|
131
|
-
border-radius: .125rem;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
107
|
code {
|
|
135
108
|
display: flex;
|
|
136
109
|
align-items: center;
|
|
@@ -138,7 +111,16 @@
|
|
|
138
111
|
}
|
|
139
112
|
|
|
140
113
|
li:not(:first-child) {
|
|
141
|
-
border-top: 1px solid var(--
|
|
114
|
+
border-top: 1px solid light-dark(var(--gray-100), var(--gray-800));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.empty-state {
|
|
118
|
+
display: none;
|
|
119
|
+
font-size: .875rem;
|
|
120
|
+
font-weight: 300;
|
|
121
|
+
color: light-dark(var(--gray-400), var(--gray-500));
|
|
122
|
+
|
|
123
|
+
&:only-child { display: block; }
|
|
142
124
|
}
|
|
143
125
|
}
|
|
144
126
|
|
|
@@ -152,7 +134,7 @@
|
|
|
152
134
|
width: .5rem;
|
|
153
135
|
aspect-ratio: 1/1;
|
|
154
136
|
background-color: currentColor;
|
|
155
|
-
border:
|
|
137
|
+
border: 1.75px solid oklch(from currentColor .9 c h / .8);
|
|
156
138
|
border-radius: .5rem;
|
|
157
139
|
}
|
|
158
140
|
|
|
@@ -163,39 +145,180 @@
|
|
|
163
145
|
&[data-status="failed"]::before { color: oklch(60% .2 25); }
|
|
164
146
|
}
|
|
165
147
|
|
|
148
|
+
.filters {
|
|
149
|
+
display: flex;
|
|
150
|
+
align-items: center;
|
|
151
|
+
gap: 2rem;
|
|
152
|
+
|
|
153
|
+
fieldset {
|
|
154
|
+
border: 0;
|
|
155
|
+
|
|
156
|
+
legend {
|
|
157
|
+
font-size: .875rem;
|
|
158
|
+
font-weight: 400;
|
|
159
|
+
color: light-dark(var(--gray-500), var(--gray-400));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
input[type="radio"] {
|
|
163
|
+
position: absolute;
|
|
164
|
+
clip: rect(0, 0, 0, 0);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.pills {
|
|
168
|
+
display: inline-flex;
|
|
169
|
+
background-color: light-dark(var(--gray-50), var(--gray-800));
|
|
170
|
+
border: 1px solid light-dark(var(--gray-200), var(--gray-700));
|
|
171
|
+
border-radius: .3rem;
|
|
172
|
+
overflow: clip;
|
|
173
|
+
|
|
174
|
+
.pill {
|
|
175
|
+
display: inline-flex;
|
|
176
|
+
align-items: center;
|
|
177
|
+
padding: .25rem .75rem;
|
|
178
|
+
font-size: .875rem;
|
|
179
|
+
font-weight: 400;
|
|
180
|
+
border-inline-start: 1px solid light-dark(var(--gray-200), var(--gray-700));
|
|
181
|
+
cursor: pointer;
|
|
182
|
+
transition: all ease-in-out 100ms;
|
|
183
|
+
|
|
184
|
+
&:first-child { border-inline-start: 0; }
|
|
185
|
+
|
|
186
|
+
&:has(input:checked),
|
|
187
|
+
&:hover:not(:has(input:checked)) {
|
|
188
|
+
background-color: light-dark(var(--gray-100), var(--gray-700));
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
select {
|
|
195
|
+
padding: .4rem .75rem;
|
|
196
|
+
font-size: .875rem;
|
|
197
|
+
background-color: light-dark(var(--gray-50), var(--gray-800));
|
|
198
|
+
border: 1px solid light-dark(var(--gray-200), var(--gray-700));
|
|
199
|
+
border-radius: .25rem;
|
|
200
|
+
cursor: pointer;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
[type=submit] { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border-width: 0; }
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
.details {
|
|
207
|
+
display: grid;
|
|
208
|
+
grid-template-columns: auto 1fr;
|
|
209
|
+
gap: 1rem 2rem;
|
|
210
|
+
overflow: hidden;
|
|
211
|
+
|
|
212
|
+
.description {
|
|
213
|
+
display: flex;
|
|
214
|
+
align-items: baseline;
|
|
215
|
+
margin-block-start: .125em;
|
|
216
|
+
font-weight: 500;
|
|
217
|
+
color: light-dark(var(--gray-700), var(--gray-400));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
.icon {
|
|
221
|
+
width: 1.125rem;
|
|
222
|
+
aspect-ratio: 1/1;
|
|
223
|
+
margin-right: .25rem;
|
|
224
|
+
border-radius: .15rem;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
.value {
|
|
228
|
+
position: relative;
|
|
229
|
+
display: flex;
|
|
230
|
+
align-items: center;
|
|
231
|
+
overflow-x: auto;
|
|
232
|
+
|
|
233
|
+
button { margin-left: .25rem; }
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
.actions {
|
|
237
|
+
display: flex;
|
|
238
|
+
align-items: center;
|
|
239
|
+
gap: .5rem;
|
|
240
|
+
position: absolute;
|
|
241
|
+
top: .5rem; right: .5rem;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
svg {
|
|
245
|
+
width: 1rem;
|
|
246
|
+
aspect-ratio: 1 / 1;
|
|
247
|
+
color: light-dark(var(--gray-800), var(--gray-200));
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
dd .copy-button .success-icon {
|
|
251
|
+
display: none;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
dd:has([data-copy-success="true"]) .copy-button .copy-icon {
|
|
255
|
+
display: none;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
dd:has([data-copy-success="true"]) .copy-button .success-icon {
|
|
259
|
+
display: block;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
166
263
|
button,
|
|
167
264
|
.copy-button {
|
|
168
265
|
padding: .125rem;
|
|
169
|
-
background: var(--
|
|
266
|
+
background: light-dark(var(--gray-50), var(--gray-800));
|
|
170
267
|
border: 0;
|
|
171
268
|
border-radius: .25rem;
|
|
172
269
|
cursor: pointer;
|
|
173
270
|
|
|
174
271
|
&:hover {
|
|
175
|
-
color:
|
|
272
|
+
color: light-dark(var(--gray-800), var(--gray-400));
|
|
176
273
|
transition: transform ease-in-out 300ms;
|
|
177
|
-
|
|
274
|
+
|
|
275
|
+
& svg { transform: scale(1.05); }
|
|
178
276
|
}
|
|
179
277
|
}
|
|
180
278
|
|
|
279
|
+
.retry-button {
|
|
280
|
+
display: inline-flex;
|
|
281
|
+
align-items: center;
|
|
282
|
+
gap: .25rem;
|
|
283
|
+
margin-inline-start: .5rem;
|
|
284
|
+
padding: .25rem .75rem;
|
|
285
|
+
border: 1px solid light-dark(var(--gray-200), var(--gray-700));
|
|
286
|
+
}
|
|
287
|
+
|
|
181
288
|
.json-highlight {
|
|
182
289
|
.json-key {
|
|
183
|
-
color: oklch(35% .15 250);
|
|
184
|
-
|
|
290
|
+
color: light-dark(oklch(35% .15 250), oklch(70% .2 250));
|
|
291
|
+
|
|
292
|
+
#payload & {
|
|
293
|
+
border-radius: .125rem;
|
|
294
|
+
cursor: pointer;
|
|
295
|
+
}
|
|
185
296
|
|
|
186
|
-
&:hover {
|
|
187
|
-
background: oklch(
|
|
297
|
+
#payload &:hover {
|
|
298
|
+
background: light-dark(oklch(from currentColor l c h / .1), oklch(from currentColor l c h / .2));
|
|
188
299
|
}
|
|
189
300
|
}
|
|
190
301
|
|
|
191
302
|
.json-key-selected {
|
|
192
|
-
background: oklch(85%
|
|
303
|
+
background: light-dark(oklch(85% .05 250), oklch(25% .08 250));
|
|
193
304
|
}
|
|
194
305
|
|
|
195
|
-
.json-string { color: oklch(30% .18 160); }
|
|
196
|
-
.json-number { color: oklch(40% .15 130); }
|
|
197
|
-
.json-boolean { color: oklch(45% .18 260); }
|
|
198
|
-
.json-null { color: oklch(50% .05 250); }
|
|
199
|
-
.json-punctuation { color: oklch(20% .03 250); }
|
|
306
|
+
.json-string { color: light-dark(oklch(30% .18 160), oklch(72% .18 160)); user-select: all; }
|
|
307
|
+
.json-number { color: light-dark(oklch(40% .15 130), oklch(70% .18 130)); }
|
|
308
|
+
.json-boolean { color: light-dark(oklch(45% .18 260), oklch(75% .2 260)); }
|
|
309
|
+
.json-null { color: light-dark(oklch(50% .05 250), oklch(68% .08 250)); }
|
|
310
|
+
.json-punctuation { color: light-dark(oklch(20% .03 250), oklch(60% .03 250)); }
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
.errors {
|
|
314
|
+
padding-block: .5rem;
|
|
315
|
+
padding-inline: 1rem;
|
|
316
|
+
max-height: calc(20lh + 1rem);
|
|
317
|
+
font-size: .875rem;
|
|
318
|
+
color: light-dark(oklch(50.5% .213 27.52), oklch(88.5% .062 18.33));
|
|
319
|
+
background: light-dark(oklch(97.1% .013 17.38), oklch(25.8% .092 26.04 / .5));
|
|
320
|
+
border-radius: .5rem;
|
|
321
|
+
overflow-x: auto;
|
|
322
|
+
font-family: "Courier New", Courier, monospace;
|
|
200
323
|
}
|
|
201
324
|
}
|