django-admin-react 1.4.12__tar.gz → 1.5.0__tar.gz

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.
Files changed (28) hide show
  1. {django_admin_react-1.4.12 → django_admin_react-1.5.0}/PKG-INFO +159 -9
  2. {django_admin_react-1.4.12 → django_admin_react-1.5.0}/README.md +154 -5
  3. {django_admin_react-1.4.12 → django_admin_react-1.5.0}/django_admin_react/conf.py +17 -4
  4. {django_admin_react-1.4.12 → django_admin_react-1.5.0}/django_admin_react/static/admin_react/.vite/manifest.json +4 -4
  5. django_admin_react-1.4.12/django_admin_react/static/admin_react/assets/ColumnLayoutModal-CLIzQjlm.js → django_admin_react-1.5.0/django_admin_react/static/admin_react/assets/ColumnLayoutModal-Bqf6vN1j.js +1 -1
  6. django_admin_react-1.4.12/django_admin_react/static/admin_react/assets/JsonViewer-B0UvsOqx.js → django_admin_react-1.5.0/django_admin_react/static/admin_react/assets/JsonViewer-DkeJkG12.js +1 -1
  7. django_admin_react-1.4.12/django_admin_react/static/admin_react/assets/index-C3ZSD987.js → django_admin_react-1.5.0/django_admin_react/static/admin_react/assets/index-CGh8t5dj.js +6 -6
  8. django_admin_react-1.5.0/django_admin_react/static/admin_react/assets/index-CpxUkcdm.css +1 -0
  9. {django_admin_react-1.4.12 → django_admin_react-1.5.0}/django_admin_react/static/admin_react/index.html +2 -2
  10. {django_admin_react-1.4.12 → django_admin_react-1.5.0}/django_admin_react/views.py +21 -6
  11. {django_admin_react-1.4.12 → django_admin_react-1.5.0}/pyproject.toml +13 -10
  12. django_admin_react-1.4.12/django_admin_react/static/admin_react/assets/index-DRQ2gAuA.css +0 -1
  13. {django_admin_react-1.4.12 → django_admin_react-1.5.0}/LICENSE +0 -0
  14. {django_admin_react-1.4.12 → django_admin_react-1.5.0}/django_admin_react/README.md +0 -0
  15. {django_admin_react-1.4.12 → django_admin_react-1.5.0}/django_admin_react/__init__.py +0 -0
  16. {django_admin_react-1.4.12 → django_admin_react-1.5.0}/django_admin_react/apps.py +0 -0
  17. {django_admin_react-1.4.12 → django_admin_react-1.5.0}/django_admin_react/audit.py +0 -0
  18. {django_admin_react-1.4.12 → django_admin_react-1.5.0}/django_admin_react/pwa.py +0 -0
  19. {django_admin_react-1.4.12 → django_admin_react-1.5.0}/django_admin_react/templates/README.md +0 -0
  20. {django_admin_react-1.4.12 → django_admin_react-1.5.0}/django_admin_react/templates/admin/base_site.html +0 -0
  21. {django_admin_react-1.4.12 → django_admin_react-1.5.0}/django_admin_react/templates/admin_react/README.md +0 -0
  22. {django_admin_react-1.4.12 → django_admin_react-1.5.0}/django_admin_react/templates/admin_react/index.html +0 -0
  23. {django_admin_react-1.4.12 → django_admin_react-1.5.0}/django_admin_react/templates/admin_react/login.html +0 -0
  24. {django_admin_react-1.4.12 → django_admin_react-1.5.0}/django_admin_react/templates/admin_react/sw.js +0 -0
  25. {django_admin_react-1.4.12 → django_admin_react-1.5.0}/django_admin_react/templates/django_admin_react/_experience_toggle_strip.html +0 -0
  26. {django_admin_react-1.4.12 → django_admin_react-1.5.0}/django_admin_react/templatetags/__init__.py +0 -0
  27. {django_admin_react-1.4.12 → django_admin_react-1.5.0}/django_admin_react/templatetags/experience_toggle.py +0 -0
  28. {django_admin_react-1.4.12 → django_admin_react-1.5.0}/django_admin_react/urls.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-admin-react
3
- Version: 1.4.12
3
+ Version: 1.5.0
4
4
  Summary: A drop-in React single-page admin for Django, driven entirely by ModelAdmin.
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -10,6 +10,7 @@ Requires-Python: >=3.10,<4.0
10
10
  Classifier: Development Status :: 5 - Production/Stable
11
11
  Classifier: Environment :: Web Environment
12
12
  Classifier: Framework :: Django
13
+ Classifier: Framework :: Django :: 4.2
13
14
  Classifier: Framework :: Django :: 5.0
14
15
  Classifier: Framework :: Django :: 5.1
15
16
  Classifier: Framework :: Django :: 5.2
@@ -25,9 +26,9 @@ Classifier: Programming Language :: Python :: 3.13
25
26
  Classifier: Programming Language :: Python :: 3.14
26
27
  Classifier: Topic :: Internet :: WWW/HTTP :: Site Management
27
28
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
28
- Requires-Dist: django (>=5.0,<7.0)
29
- Requires-Dist: django-admin-mcp-api (>=1.0.0,<2.0.0)
30
- Requires-Dist: django-admin-rest-api (>=1.0.6,<2.0.0)
29
+ Requires-Dist: django (>=4.2,<7.0)
30
+ Requires-Dist: django-admin-mcp-api (>=1.1.0,<2.0.0)
31
+ Requires-Dist: django-admin-rest-api (>=1.1.0,<2.0.0)
31
32
  Project-URL: Documentation, https://github.com/MartinCastroAlvarez/django-admin-react#readme
32
33
  Project-URL: Homepage, https://github.com/MartinCastroAlvarez/django-admin-react
33
34
  Project-URL: Repository, https://github.com/MartinCastroAlvarez/django-admin-react
@@ -190,9 +191,12 @@ DJANGO_ADMIN_REACT = {
190
191
  "BRAND_LOGO_URL": None, # str | None — favicon + sidebar logo;
191
192
  # falls back to AdminSite.site_logo. Absolute
192
193
  # URL or a path under your STATIC_URL.
193
- "PRIMARY_COLOR": "#2563eb", # accent for primary buttons, links, and
194
- # active states. Hex only (validated);
195
- # injected as the --dar-primary CSS var, so
194
+ "PRIMARY_COLOR": None, # accent for primary buttons, links, and
195
+ # active states (#437 / #631). Hex only
196
+ # (validated). None reads
197
+ # `site_primary_color` off your AdminSite;
198
+ # fallback default is "#2563eb". Injected
199
+ # as the --dar-primary CSS var, so
196
200
  # rebranding needs no React rebuild.
197
201
 
198
202
  # Auth + API mount
@@ -247,10 +251,39 @@ Both values are written into the SPA index template as standard
247
251
  reads them at boot, so the first paint already carries the consumer's
248
252
  brand. No flash of the package's defaults.
249
253
 
254
+ #### Accent colour (`PRIMARY_COLOR` + `AdminSite.site_primary_color`)
255
+
256
+ `PRIMARY_COLOR` defaults to `None` so a custom `AdminSite` subclass can
257
+ own the brand colour the same way it owns `site_header` / `site_logo`
258
+ (#631). Resolution order — explicit setting wins, AdminSite is the
259
+ structural default, built-in fallback last:
260
+
261
+ 1. `DJANGO_ADMIN_REACT["PRIMARY_COLOR"]` — explicit per-deployment override.
262
+ 2. `<your AdminSite>.site_primary_color` — convention attribute on your
263
+ custom `AdminSite` subclass (Django has no such attribute by default;
264
+ add it as a constant alongside `site_header` / `site_logo`).
265
+ 3. `"#2563eb"` — the package's last-resort fallback.
266
+
267
+ Every layer runs through a strict hex-colour regex (`#rgb` / `#rgba` /
268
+ `#rrggbb` / `#rrggbbaa`) before being injected into the SPA's `<style>`
269
+ block, so a non-hex value at any layer falls through to the next — CSS
270
+ injection is impossible at any source.
271
+
272
+ ```python
273
+ # myproject/admin.py
274
+ from django.contrib.admin import AdminSite
275
+
276
+ class AcmeAdminSite(AdminSite):
277
+ site_header = "Acme"
278
+ site_title = "Acme Admin"
279
+ site_logo = "/static/acme/logo.svg"
280
+ site_primary_color = "#10b981" # emerald — used by legacy admin AND the SPA
281
+ ```
282
+
250
283
  ### Requirements
251
284
 
252
285
  - **Python**: 3.10+
253
- - **Django**: 5.0, 5.1, 5.2, 6.0 (and any later 6.x)
286
+ - **Django**: 4.2 LTS, 5.0, 5.1, 5.2 LTS, 6.0 (and any later 6.x)
254
287
  - **Database**: anything Django supports — the package is ORM-only,
255
288
  no direct SQL.
256
289
  - **Auth**: Django's built-in session + CSRF. Works with custom
@@ -620,7 +653,9 @@ all share the v1 wire contract. Per-feature live status below.
620
653
  | `date_hierarchy` | ✅ |
621
654
  | `list_editable` + bulk PATCH | ✅ |
622
655
  | `actions` — batch + detail (signature-classified) | ✅ |
623
- | `autocomplete_fields` / `raw_id_fields` | ✅ |
656
+ | `autocomplete_fields` | ✅ |
657
+ | `raw_id_fields` (pk text input + lookup popup) | 🟡 [#626](https://github.com/MartinCastroAlvarez/django-admin-react/issues/626) (API emits the hint; SPA still renders autocomplete) |
658
+ | `radio_fields` (inline radio buttons vs `<select>`) | 🟡 [#626](https://github.com/MartinCastroAlvarez/django-admin-react/issues/626) (API emits the hint; SPA still renders dropdown) |
624
659
  | `ManyToManyField` read + write | ✅ |
625
660
  | `inlines` (TabularInline / StackedInline) — read + write | ✅ |
626
661
  | `FileField` / `ImageField` — read | ✅ |
@@ -636,6 +671,121 @@ all share the v1 wire contract. Per-feature live status below.
636
671
 
637
672
  ✅ = shipped. 🟡 = not yet built (tracked).
638
673
 
674
+ ### Stock-Django `ModelAdmin` hooks that do NOT carry through to the SPA
675
+
676
+ The SPA renders from the JSON wire — it never sees the consumer's
677
+ Django HTML templates, custom widgets, or `get_urls()` views. The
678
+ hooks below are stock-Django extension points the SPA cannot honour
679
+ today; if your admin uses any of them, the surface behaves
680
+ differently on the SPA than on the legacy `/admin/`. Tracking
681
+ issues link the work to close each gap.
682
+
683
+ | Stock-Django hook | SPA behaviour | Tracked |
684
+ |---|---|---|
685
+ | `change_form_template` / `change_list_template` / `add_form_template` / `change_password_template` / `object_history_template` overrides | Silently ignored — the SPA renders entirely from the JSON wire. | [#624](https://github.com/MartinCastroAlvarez/django-admin-react/issues/624) |
686
+ | `formfield_overrides = {Field: {"widget": CustomWidget}}` | Custom widget invisible — the SPA picks its own control from the field's `type`. No React-side widget-registration API yet. | [#625](https://github.com/MartinCastroAlvarez/django-admin-react/issues/625) |
687
+ | `raw_id_fields` | Falls back to the autocomplete picker (same as `autocomplete_fields`). Defeats the purpose for FKs with 10M+ rows where autocomplete `get_search_results` is too expensive. | [#626](https://github.com/MartinCastroAlvarez/django-admin-react/issues/626) |
688
+ | `radio_fields = {"status": admin.HORIZONTAL}` | Renders a `<select>` (default choice control) instead of inline radio buttons. | [#626](https://github.com/MartinCastroAlvarez/django-admin-react/issues/626) |
689
+ | `filter_horizontal` / `filter_vertical` (M2M shuttle widget) | Renders the generic multi-select checkbox list, not Django's two-pane shuttle. Switch the field to `autocomplete_fields` for a workable SPA UX. | [#627](https://github.com/MartinCastroAlvarez/django-admin-react/issues/627) |
690
+ | `GenericForeignKey` / `GenericInlineModelAdmin` | Support gap — verify per-model before relying on the SPA. | [#628](https://github.com/MartinCastroAlvarez/django-admin-react/issues/628) |
691
+ | `LANGUAGE_CODE` / `gettext` / `Accept-Language` | The SPA chrome stays English; translated `verbose_name` / `help_text` / `@admin.action(description=_("..."))` are not surfaced per-request. | [#630](https://github.com/MartinCastroAlvarez/django-admin-react/issues/630) |
692
+ | `ModelAdmin.get_urls()` custom views | Opens as a popout (`<a target="_blank">`) into the Django-rendered HTML page — no SPA chrome, no breadcrumb. The link IS surfaced; the UX is just outside the SPA. | [#623](https://github.com/MartinCastroAlvarez/django-admin-react/issues/623) |
693
+ | Django 4.2 LTS support | Not yet — the package pins `django >= 5.0,<7.0`. | [#622](https://github.com/MartinCastroAlvarez/django-admin-react/issues/622) |
694
+
695
+ If your admin relies on any "silently ignored" hook above, the
696
+ typical workaround is to keep that model on the legacy
697
+ `/admin/` surface via the
698
+ [experience-toggle strip](#experience-toggle-strip-optional) — the
699
+ SPA + legacy admin happily coexist.
700
+
701
+ ---
702
+
703
+ ## Writing safe `list_display` callables
704
+
705
+ This applies on **both** the legacy `/admin/` and the SPA — but the
706
+ SPA renders any `format_html` / `mark_safe` value via React's
707
+ `dangerouslySetInnerHTML`, so misuse is reflected XSS the same way
708
+ the legacy admin would be.
709
+
710
+ **Do not** interpolate user-controlled data into a `mark_safe(...)`
711
+ string. The whole point of `mark_safe` is "I have already escaped
712
+ this," and `f"<span>{obj.user_input}</span>"` has not — so a
713
+ `user_input` of `<script>alert(1)</script>` runs.
714
+
715
+ ```python
716
+ # WRONG — copy-paste-from-StackOverflow XSS hazard.
717
+ @admin.display(description="Status")
718
+ def status_badge(self, obj):
719
+ return mark_safe(f'<span class="badge">{obj.user_input}</span>')
720
+
721
+ # RIGHT — format_html auto-escapes every interpolated arg.
722
+ @admin.display(description="Status")
723
+ def status_badge(self, obj):
724
+ return format_html('<span class="badge">{}</span>', obj.user_input)
725
+ ```
726
+
727
+ Same rule for `readonly_fields` callables. See
728
+ [#633](https://github.com/MartinCastroAlvarez/django-admin-react/issues/633)
729
+ for the optional defense-in-depth `STRICT_HTML` setting tracking
730
+ issue (bleach-clean every rendered HTML value with a tight allow-list).
731
+
732
+ ---
733
+
734
+ ## Hardening
735
+
736
+ ### Brute-force defense on `/api/v1/login/`
737
+
738
+ The package's React login endpoint (`<mount>/api/v1/login/`) reuses
739
+ Django's session auth, so the canonical brute-force defenses work
740
+ unchanged. The recommended layer is
741
+ [`django-axes`](https://pypi.org/project/django-axes/):
742
+
743
+ ```python
744
+ # settings.py
745
+ INSTALLED_APPS = [..., "axes", "django_admin_react", "django_admin_rest_api"]
746
+
747
+ AUTHENTICATION_BACKENDS = [
748
+ "axes.backends.AxesStandaloneBackend",
749
+ "django.contrib.auth.backends.ModelBackend",
750
+ ]
751
+ MIDDLEWARE = [..., "axes.middleware.AxesMiddleware"]
752
+
753
+ AXES_FAILURE_LIMIT = 5
754
+ AXES_COOLOFF_TIME = 1 # hour
755
+ ```
756
+
757
+ Axes intercepts via `AUTHENTICATION_BACKENDS`, not URL middleware, so
758
+ lockouts apply to both the legacy admin login and the SPA's JSON
759
+ login automatically. Tracked: [#634](https://github.com/MartinCastroAlvarez/django-admin-react/issues/634).
760
+
761
+ ### Mounting the API on a different origin (CORS + cookies)
762
+
763
+ `DJANGO_ADMIN_REACT["API_URL_PREFIX"]` lets the SPA point at a
764
+ separately-mounted REST API — e.g. SPA at `admin.example.com`
765
+ talking to an API at `api.example.com`. The session-cookie auth
766
+ across origins needs three settings configured together; if any
767
+ one is missing, every API call silently 401s after login.
768
+
769
+ ```python
770
+ # settings.py — required when SPA and API are on different origins.
771
+ SESSION_COOKIE_SAMESITE = "None" # default "Lax" drops cookies cross-origin
772
+ SESSION_COOKIE_SECURE = True # required by browsers when SameSite=None
773
+ CSRF_COOKIE_SAMESITE = "None"
774
+ CSRF_COOKIE_SECURE = True
775
+
776
+ # pip install django-cors-headers
777
+ INSTALLED_APPS = [..., "corsheaders", ...]
778
+ MIDDLEWARE = ["corsheaders.middleware.CorsMiddleware", ...]
779
+
780
+ CORS_ALLOW_CREDENTIALS = True
781
+ CORS_ALLOWED_ORIGINS = ["https://admin.example.com"] # NEVER "*" with credentials
782
+ CSRF_TRUSTED_ORIGINS = ["https://admin.example.com"]
783
+ ```
784
+
785
+ The SPA's HTTP client already sends `credentials: "include"`, so no
786
+ frontend change is needed — only the Django-side cookie + CORS
787
+ config above. Tracked: [#635](https://github.com/MartinCastroAlvarez/django-admin-react/issues/635).
788
+
639
789
  ---
640
790
 
641
791
  ## The API surface
@@ -155,9 +155,12 @@ DJANGO_ADMIN_REACT = {
155
155
  "BRAND_LOGO_URL": None, # str | None — favicon + sidebar logo;
156
156
  # falls back to AdminSite.site_logo. Absolute
157
157
  # URL or a path under your STATIC_URL.
158
- "PRIMARY_COLOR": "#2563eb", # accent for primary buttons, links, and
159
- # active states. Hex only (validated);
160
- # injected as the --dar-primary CSS var, so
158
+ "PRIMARY_COLOR": None, # accent for primary buttons, links, and
159
+ # active states (#437 / #631). Hex only
160
+ # (validated). None reads
161
+ # `site_primary_color` off your AdminSite;
162
+ # fallback default is "#2563eb". Injected
163
+ # as the --dar-primary CSS var, so
161
164
  # rebranding needs no React rebuild.
162
165
 
163
166
  # Auth + API mount
@@ -212,10 +215,39 @@ Both values are written into the SPA index template as standard
212
215
  reads them at boot, so the first paint already carries the consumer's
213
216
  brand. No flash of the package's defaults.
214
217
 
218
+ #### Accent colour (`PRIMARY_COLOR` + `AdminSite.site_primary_color`)
219
+
220
+ `PRIMARY_COLOR` defaults to `None` so a custom `AdminSite` subclass can
221
+ own the brand colour the same way it owns `site_header` / `site_logo`
222
+ (#631). Resolution order — explicit setting wins, AdminSite is the
223
+ structural default, built-in fallback last:
224
+
225
+ 1. `DJANGO_ADMIN_REACT["PRIMARY_COLOR"]` — explicit per-deployment override.
226
+ 2. `<your AdminSite>.site_primary_color` — convention attribute on your
227
+ custom `AdminSite` subclass (Django has no such attribute by default;
228
+ add it as a constant alongside `site_header` / `site_logo`).
229
+ 3. `"#2563eb"` — the package's last-resort fallback.
230
+
231
+ Every layer runs through a strict hex-colour regex (`#rgb` / `#rgba` /
232
+ `#rrggbb` / `#rrggbbaa`) before being injected into the SPA's `<style>`
233
+ block, so a non-hex value at any layer falls through to the next — CSS
234
+ injection is impossible at any source.
235
+
236
+ ```python
237
+ # myproject/admin.py
238
+ from django.contrib.admin import AdminSite
239
+
240
+ class AcmeAdminSite(AdminSite):
241
+ site_header = "Acme"
242
+ site_title = "Acme Admin"
243
+ site_logo = "/static/acme/logo.svg"
244
+ site_primary_color = "#10b981" # emerald — used by legacy admin AND the SPA
245
+ ```
246
+
215
247
  ### Requirements
216
248
 
217
249
  - **Python**: 3.10+
218
- - **Django**: 5.0, 5.1, 5.2, 6.0 (and any later 6.x)
250
+ - **Django**: 4.2 LTS, 5.0, 5.1, 5.2 LTS, 6.0 (and any later 6.x)
219
251
  - **Database**: anything Django supports — the package is ORM-only,
220
252
  no direct SQL.
221
253
  - **Auth**: Django's built-in session + CSRF. Works with custom
@@ -585,7 +617,9 @@ all share the v1 wire contract. Per-feature live status below.
585
617
  | `date_hierarchy` | ✅ |
586
618
  | `list_editable` + bulk PATCH | ✅ |
587
619
  | `actions` — batch + detail (signature-classified) | ✅ |
588
- | `autocomplete_fields` / `raw_id_fields` | ✅ |
620
+ | `autocomplete_fields` | ✅ |
621
+ | `raw_id_fields` (pk text input + lookup popup) | 🟡 [#626](https://github.com/MartinCastroAlvarez/django-admin-react/issues/626) (API emits the hint; SPA still renders autocomplete) |
622
+ | `radio_fields` (inline radio buttons vs `<select>`) | 🟡 [#626](https://github.com/MartinCastroAlvarez/django-admin-react/issues/626) (API emits the hint; SPA still renders dropdown) |
589
623
  | `ManyToManyField` read + write | ✅ |
590
624
  | `inlines` (TabularInline / StackedInline) — read + write | ✅ |
591
625
  | `FileField` / `ImageField` — read | ✅ |
@@ -601,6 +635,121 @@ all share the v1 wire contract. Per-feature live status below.
601
635
 
602
636
  ✅ = shipped. 🟡 = not yet built (tracked).
603
637
 
638
+ ### Stock-Django `ModelAdmin` hooks that do NOT carry through to the SPA
639
+
640
+ The SPA renders from the JSON wire — it never sees the consumer's
641
+ Django HTML templates, custom widgets, or `get_urls()` views. The
642
+ hooks below are stock-Django extension points the SPA cannot honour
643
+ today; if your admin uses any of them, the surface behaves
644
+ differently on the SPA than on the legacy `/admin/`. Tracking
645
+ issues link the work to close each gap.
646
+
647
+ | Stock-Django hook | SPA behaviour | Tracked |
648
+ |---|---|---|
649
+ | `change_form_template` / `change_list_template` / `add_form_template` / `change_password_template` / `object_history_template` overrides | Silently ignored — the SPA renders entirely from the JSON wire. | [#624](https://github.com/MartinCastroAlvarez/django-admin-react/issues/624) |
650
+ | `formfield_overrides = {Field: {"widget": CustomWidget}}` | Custom widget invisible — the SPA picks its own control from the field's `type`. No React-side widget-registration API yet. | [#625](https://github.com/MartinCastroAlvarez/django-admin-react/issues/625) |
651
+ | `raw_id_fields` | Falls back to the autocomplete picker (same as `autocomplete_fields`). Defeats the purpose for FKs with 10M+ rows where autocomplete `get_search_results` is too expensive. | [#626](https://github.com/MartinCastroAlvarez/django-admin-react/issues/626) |
652
+ | `radio_fields = {"status": admin.HORIZONTAL}` | Renders a `<select>` (default choice control) instead of inline radio buttons. | [#626](https://github.com/MartinCastroAlvarez/django-admin-react/issues/626) |
653
+ | `filter_horizontal` / `filter_vertical` (M2M shuttle widget) | Renders the generic multi-select checkbox list, not Django's two-pane shuttle. Switch the field to `autocomplete_fields` for a workable SPA UX. | [#627](https://github.com/MartinCastroAlvarez/django-admin-react/issues/627) |
654
+ | `GenericForeignKey` / `GenericInlineModelAdmin` | Support gap — verify per-model before relying on the SPA. | [#628](https://github.com/MartinCastroAlvarez/django-admin-react/issues/628) |
655
+ | `LANGUAGE_CODE` / `gettext` / `Accept-Language` | The SPA chrome stays English; translated `verbose_name` / `help_text` / `@admin.action(description=_("..."))` are not surfaced per-request. | [#630](https://github.com/MartinCastroAlvarez/django-admin-react/issues/630) |
656
+ | `ModelAdmin.get_urls()` custom views | Opens as a popout (`<a target="_blank">`) into the Django-rendered HTML page — no SPA chrome, no breadcrumb. The link IS surfaced; the UX is just outside the SPA. | [#623](https://github.com/MartinCastroAlvarez/django-admin-react/issues/623) |
657
+ | Django 4.2 LTS support | Not yet — the package pins `django >= 5.0,<7.0`. | [#622](https://github.com/MartinCastroAlvarez/django-admin-react/issues/622) |
658
+
659
+ If your admin relies on any "silently ignored" hook above, the
660
+ typical workaround is to keep that model on the legacy
661
+ `/admin/` surface via the
662
+ [experience-toggle strip](#experience-toggle-strip-optional) — the
663
+ SPA + legacy admin happily coexist.
664
+
665
+ ---
666
+
667
+ ## Writing safe `list_display` callables
668
+
669
+ This applies on **both** the legacy `/admin/` and the SPA — but the
670
+ SPA renders any `format_html` / `mark_safe` value via React's
671
+ `dangerouslySetInnerHTML`, so misuse is reflected XSS the same way
672
+ the legacy admin would be.
673
+
674
+ **Do not** interpolate user-controlled data into a `mark_safe(...)`
675
+ string. The whole point of `mark_safe` is "I have already escaped
676
+ this," and `f"<span>{obj.user_input}</span>"` has not — so a
677
+ `user_input` of `<script>alert(1)</script>` runs.
678
+
679
+ ```python
680
+ # WRONG — copy-paste-from-StackOverflow XSS hazard.
681
+ @admin.display(description="Status")
682
+ def status_badge(self, obj):
683
+ return mark_safe(f'<span class="badge">{obj.user_input}</span>')
684
+
685
+ # RIGHT — format_html auto-escapes every interpolated arg.
686
+ @admin.display(description="Status")
687
+ def status_badge(self, obj):
688
+ return format_html('<span class="badge">{}</span>', obj.user_input)
689
+ ```
690
+
691
+ Same rule for `readonly_fields` callables. See
692
+ [#633](https://github.com/MartinCastroAlvarez/django-admin-react/issues/633)
693
+ for the optional defense-in-depth `STRICT_HTML` setting tracking
694
+ issue (bleach-clean every rendered HTML value with a tight allow-list).
695
+
696
+ ---
697
+
698
+ ## Hardening
699
+
700
+ ### Brute-force defense on `/api/v1/login/`
701
+
702
+ The package's React login endpoint (`<mount>/api/v1/login/`) reuses
703
+ Django's session auth, so the canonical brute-force defenses work
704
+ unchanged. The recommended layer is
705
+ [`django-axes`](https://pypi.org/project/django-axes/):
706
+
707
+ ```python
708
+ # settings.py
709
+ INSTALLED_APPS = [..., "axes", "django_admin_react", "django_admin_rest_api"]
710
+
711
+ AUTHENTICATION_BACKENDS = [
712
+ "axes.backends.AxesStandaloneBackend",
713
+ "django.contrib.auth.backends.ModelBackend",
714
+ ]
715
+ MIDDLEWARE = [..., "axes.middleware.AxesMiddleware"]
716
+
717
+ AXES_FAILURE_LIMIT = 5
718
+ AXES_COOLOFF_TIME = 1 # hour
719
+ ```
720
+
721
+ Axes intercepts via `AUTHENTICATION_BACKENDS`, not URL middleware, so
722
+ lockouts apply to both the legacy admin login and the SPA's JSON
723
+ login automatically. Tracked: [#634](https://github.com/MartinCastroAlvarez/django-admin-react/issues/634).
724
+
725
+ ### Mounting the API on a different origin (CORS + cookies)
726
+
727
+ `DJANGO_ADMIN_REACT["API_URL_PREFIX"]` lets the SPA point at a
728
+ separately-mounted REST API — e.g. SPA at `admin.example.com`
729
+ talking to an API at `api.example.com`. The session-cookie auth
730
+ across origins needs three settings configured together; if any
731
+ one is missing, every API call silently 401s after login.
732
+
733
+ ```python
734
+ # settings.py — required when SPA and API are on different origins.
735
+ SESSION_COOKIE_SAMESITE = "None" # default "Lax" drops cookies cross-origin
736
+ SESSION_COOKIE_SECURE = True # required by browsers when SameSite=None
737
+ CSRF_COOKIE_SAMESITE = "None"
738
+ CSRF_COOKIE_SECURE = True
739
+
740
+ # pip install django-cors-headers
741
+ INSTALLED_APPS = [..., "corsheaders", ...]
742
+ MIDDLEWARE = ["corsheaders.middleware.CorsMiddleware", ...]
743
+
744
+ CORS_ALLOW_CREDENTIALS = True
745
+ CORS_ALLOWED_ORIGINS = ["https://admin.example.com"] # NEVER "*" with credentials
746
+ CSRF_TRUSTED_ORIGINS = ["https://admin.example.com"]
747
+ ```
748
+
749
+ The SPA's HTTP client already sends `credentials: "include"`, so no
750
+ frontend change is needed — only the Django-side cookie + CORS
751
+ config above. Tracked: [#635](https://github.com/MartinCastroAlvarez/django-admin-react/issues/635).
752
+
604
753
  ---
605
754
 
606
755
  ## The API surface
@@ -20,6 +20,12 @@ from typing import Any
20
20
 
21
21
  from django.conf import settings as django_settings
22
22
 
23
+ # Built-in fallback for the ``--dar-primary`` accent color when the
24
+ # consumer hasn't set ``PRIMARY_COLOR`` AND their ``AdminSite`` has no
25
+ # ``site_primary_color`` attribute. Re-exported so ``views.py`` can
26
+ # pick up the same constant instead of stringifying its own.
27
+ DEFAULT_PRIMARY_COLOR = "#2563eb"
28
+
23
29
  DEFAULTS: dict[str, Any] = {
24
30
  "ADMIN_SITE": "django.contrib.admin.site",
25
31
  # The list page size derives from the model's
@@ -59,9 +65,16 @@ DEFAULTS: dict[str, Any] = {
59
65
  # ``--dar-primary`` CSS variable so a consumer can brand the admin with
60
66
  # no React rebuild. Must be a hex color (``#rgb`` / ``#rgba`` /
61
67
  # ``#rrggbb`` / ``#rrggbbaa``); anything else is rejected at render and
62
- # falls back to this default, since the value is written into a
63
- # ``<style>`` block and must not be able to inject CSS.
64
- "PRIMARY_COLOR": "#2563eb",
68
+ # falls back to ``DEFAULT_PRIMARY_COLOR`` below, since the value is
69
+ # written into a ``<style>`` block and must not be able to inject CSS.
70
+ #
71
+ # ``None`` (default) means "consumer didn't explicitly set this" — the
72
+ # SPA reads ``site_primary_color`` off the configured ``AdminSite``
73
+ # next, then falls back to ``DEFAULT_PRIMARY_COLOR``. Mirrors
74
+ # ``BRAND_TITLE`` / ``BRAND_LOGO_URL``: setting wins as the
75
+ # per-deployment override, AdminSite attr is the structural default,
76
+ # built-in default last (#631).
77
+ "PRIMARY_COLOR": None,
65
78
  # ``REACT_LOGIN`` — React-rendered login is the **default** so the
66
79
  # SPA fully replaces the Django admin URL surface end-to-end (owner
67
80
  # directive 2026-05-28). ``SpaIndexView`` serves the React shell to
@@ -149,7 +162,7 @@ class _PackageSettings:
149
162
  ENABLE_PROFILING: bool = DEFAULTS["ENABLE_PROFILING"]
150
163
  BRAND_TITLE: str | None = DEFAULTS["BRAND_TITLE"]
151
164
  BRAND_LOGO_URL: str | None = DEFAULTS["BRAND_LOGO_URL"]
152
- PRIMARY_COLOR: str = DEFAULTS["PRIMARY_COLOR"]
165
+ PRIMARY_COLOR: str | None = DEFAULTS["PRIMARY_COLOR"]
153
166
  REACT_LOGIN: bool = DEFAULTS["REACT_LOGIN"]
154
167
  API_URL_PREFIX: str | None = DEFAULTS["API_URL_PREFIX"]
155
168
  LEGACY_ADMIN_URL_PREFIX: str | None = DEFAULTS["LEGACY_ADMIN_URL_PREFIX"]
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "../../packages/details/src/JsonViewer.tsx": {
3
- "file": "assets/JsonViewer-B0UvsOqx.js",
3
+ "file": "assets/JsonViewer-DkeJkG12.js",
4
4
  "name": "JsonViewer",
5
5
  "src": "../../packages/details/src/JsonViewer.tsx",
6
6
  "isDynamicEntry": true,
@@ -9,7 +9,7 @@
9
9
  ]
10
10
  },
11
11
  "index.html": {
12
- "file": "assets/index-C3ZSD987.js",
12
+ "file": "assets/index-CGh8t5dj.js",
13
13
  "name": "index",
14
14
  "src": "index.html",
15
15
  "isEntry": true,
@@ -18,11 +18,11 @@
18
18
  "src/ColumnLayoutModal.tsx"
19
19
  ],
20
20
  "css": [
21
- "assets/index-DRQ2gAuA.css"
21
+ "assets/index-CpxUkcdm.css"
22
22
  ]
23
23
  },
24
24
  "src/ColumnLayoutModal.tsx": {
25
- "file": "assets/ColumnLayoutModal-CLIzQjlm.js",
25
+ "file": "assets/ColumnLayoutModal-Bqf6vN1j.js",
26
26
  "name": "ColumnLayoutModal",
27
27
  "src": "src/ColumnLayoutModal.tsx",
28
28
  "isDynamicEntry": true,
@@ -1,4 +1,4 @@
1
- import{c as tt,d as c,R as $,r as ke,j as I,M as kn,b as On,a as Jt}from"./index-C3ZSD987.js";const Tn=[["circle",{cx:"9",cy:"12",r:"1",key:"1vctgf"}],["circle",{cx:"9",cy:"5",r:"1",key:"hp0tcf"}],["circle",{cx:"9",cy:"19",r:"1",key:"fkjjf6"}],["circle",{cx:"15",cy:"12",r:"1",key:"1tmaij"}],["circle",{cx:"15",cy:"5",r:"1",key:"19l28e"}],["circle",{cx:"15",cy:"19",r:"1",key:"f4zoj3"}]],Qt=tt("grip-vertical",Tn);const Ln=[["rect",{width:"18",height:"11",x:"3",y:"11",rx:"2",ry:"2",key:"1w4ew1"}],["path",{d:"M7 11V7a5 5 0 0 1 9.9-1",key:"1mm8w8"}]],jn=tt("lock-open",Ln);const zn=[["rect",{width:"18",height:"11",x:"3",y:"11",rx:"2",ry:"2",key:"1w4ew1"}],["path",{d:"M7 11V7a5 5 0 0 1 10 0v4",key:"fwvmzm"}]],jt=tt("lock",zn);const $n=[["path",{d:"M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8",key:"1357e3"}],["path",{d:"M3 3v5h5",key:"1xhq8a"}]],Bn=tt("rotate-ccw",$n);function Pn(){for(var e=arguments.length,t=new Array(e),n=0;n<e;n++)t[n]=arguments[n];return c.useMemo(()=>r=>{t.forEach(o=>o(r))},t)}const nt=typeof window<"u"&&typeof window.document<"u"&&typeof window.document.createElement<"u";function xe(e){const t=Object.prototype.toString.call(e);return t==="[object Window]"||t==="[object global]"}function mt(e){return"nodeType"in e}function B(e){var t,n;return e?xe(e)?e:mt(e)&&(t=(n=e.ownerDocument)==null?void 0:n.defaultView)!=null?t:window:window}function yt(e){const{Document:t}=B(e);return e instanceof t}function Pe(e){return xe(e)?!1:e instanceof B(e).HTMLElement}function Zt(e){return e instanceof B(e).SVGElement}function we(e){return e?xe(e)?e.document:mt(e)?yt(e)?e:Pe(e)||Zt(e)?e.ownerDocument:document:document:document}const Z=nt?c.useLayoutEffect:c.useEffect;function xt(e){const t=c.useRef(e);return Z(()=>{t.current=e}),c.useCallback(function(){for(var n=arguments.length,r=new Array(n),o=0;o<n;o++)r[o]=arguments[o];return t.current==null?void 0:t.current(...r)},[])}function Fn(){const e=c.useRef(null),t=c.useCallback((r,o)=>{e.current=setInterval(r,o)},[]),n=c.useCallback(()=>{e.current!==null&&(clearInterval(e.current),e.current=null)},[]);return[t,n]}function je(e,t){t===void 0&&(t=[e]);const n=c.useRef(e);return Z(()=>{n.current!==e&&(n.current=e)},t),n}function Fe(e,t){const n=c.useRef();return c.useMemo(()=>{const r=e(n.current);return n.current=r,r},[...t])}function Je(e){const t=xt(e),n=c.useRef(null),r=c.useCallback(o=>{o!==n.current&&t?.(o,n.current),n.current=o},[]);return[n,r]}function ht(e){const t=c.useRef();return c.useEffect(()=>{t.current=e},[e]),t.current}let lt={};function Xe(e,t){return c.useMemo(()=>{if(t)return t;const n=lt[e]==null?0:lt[e]+1;return lt[e]=n,e+"-"+n},[e,t])}function en(e){return function(t){for(var n=arguments.length,r=new Array(n>1?n-1:0),o=1;o<n;o++)r[o-1]=arguments[o];return r.reduce((i,s)=>{const a=Object.entries(s);for(const[l,u]of a){const f=i[l];f!=null&&(i[l]=f+e*u)}return i},{...t})}}const ye=en(1),ze=en(-1);function Xn(e){return"clientX"in e&&"clientY"in e}function wt(e){if(!e)return!1;const{KeyboardEvent:t}=B(e.target);return t&&e instanceof t}function Un(e){if(!e)return!1;const{TouchEvent:t}=B(e.target);return t&&e instanceof t}function gt(e){if(Un(e)){if(e.touches&&e.touches.length){const{clientX:t,clientY:n}=e.touches[0];return{x:t,y:n}}else if(e.changedTouches&&e.changedTouches.length){const{clientX:t,clientY:n}=e.changedTouches[0];return{x:t,y:n}}}return Xn(e)?{x:e.clientX,y:e.clientY}:null}const $e=Object.freeze({Translate:{toString(e){if(!e)return;const{x:t,y:n}=e;return"translate3d("+(t?Math.round(t):0)+"px, "+(n?Math.round(n):0)+"px, 0)"}},Scale:{toString(e){if(!e)return;const{scaleX:t,scaleY:n}=e;return"scaleX("+t+") scaleY("+n+")"}},Transform:{toString(e){if(e)return[$e.Translate.toString(e),$e.Scale.toString(e)].join(" ")}},Transition:{toString(e){let{property:t,duration:n,easing:r}=e;return t+" "+n+"ms "+r}}}),zt="a,frame,iframe,input:not([type=hidden]):not(:disabled),select:not(:disabled),textarea:not(:disabled),button:not(:disabled),*[tabindex]";function Yn(e){return e.matches(zt)?e:e.querySelector(zt)}const Wn={display:"none"};function Vn(e){let{id:t,value:n}=e;return $.createElement("div",{id:t,style:Wn},n)}function Hn(e){let{id:t,announcement:n,ariaLiveType:r="assertive"}=e;const o={position:"fixed",top:0,left:0,width:1,height:1,margin:-1,border:0,padding:0,overflow:"hidden",clip:"rect(0 0 0 0)",clipPath:"inset(100%)",whiteSpace:"nowrap"};return $.createElement("div",{id:t,style:o,role:"status","aria-live":r,"aria-atomic":!0},n)}function Kn(){const[e,t]=c.useState("");return{announce:c.useCallback(r=>{r!=null&&t(r)},[]),announcement:e}}const tn=c.createContext(null);function _n(e){const t=c.useContext(tn);c.useEffect(()=>{if(!t)throw new Error("useDndMonitor must be used within a children of <DndContext>");return t(e)},[e,t])}function qn(){const[e]=c.useState(()=>new Set),t=c.useCallback(r=>(e.add(r),()=>e.delete(r)),[e]);return[c.useCallback(r=>{let{type:o,event:i}=r;e.forEach(s=>{var a;return(a=s[o])==null?void 0:a.call(s,i)})},[e]),t]}const Gn={draggable:`
1
+ import{c as tt,d as c,R as $,r as ke,j as I,M as kn,b as On,a as Jt}from"./index-CGh8t5dj.js";const Tn=[["circle",{cx:"9",cy:"12",r:"1",key:"1vctgf"}],["circle",{cx:"9",cy:"5",r:"1",key:"hp0tcf"}],["circle",{cx:"9",cy:"19",r:"1",key:"fkjjf6"}],["circle",{cx:"15",cy:"12",r:"1",key:"1tmaij"}],["circle",{cx:"15",cy:"5",r:"1",key:"19l28e"}],["circle",{cx:"15",cy:"19",r:"1",key:"f4zoj3"}]],Qt=tt("grip-vertical",Tn);const Ln=[["rect",{width:"18",height:"11",x:"3",y:"11",rx:"2",ry:"2",key:"1w4ew1"}],["path",{d:"M7 11V7a5 5 0 0 1 9.9-1",key:"1mm8w8"}]],jn=tt("lock-open",Ln);const zn=[["rect",{width:"18",height:"11",x:"3",y:"11",rx:"2",ry:"2",key:"1w4ew1"}],["path",{d:"M7 11V7a5 5 0 0 1 10 0v4",key:"fwvmzm"}]],jt=tt("lock",zn);const $n=[["path",{d:"M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8",key:"1357e3"}],["path",{d:"M3 3v5h5",key:"1xhq8a"}]],Bn=tt("rotate-ccw",$n);function Pn(){for(var e=arguments.length,t=new Array(e),n=0;n<e;n++)t[n]=arguments[n];return c.useMemo(()=>r=>{t.forEach(o=>o(r))},t)}const nt=typeof window<"u"&&typeof window.document<"u"&&typeof window.document.createElement<"u";function xe(e){const t=Object.prototype.toString.call(e);return t==="[object Window]"||t==="[object global]"}function mt(e){return"nodeType"in e}function B(e){var t,n;return e?xe(e)?e:mt(e)&&(t=(n=e.ownerDocument)==null?void 0:n.defaultView)!=null?t:window:window}function yt(e){const{Document:t}=B(e);return e instanceof t}function Pe(e){return xe(e)?!1:e instanceof B(e).HTMLElement}function Zt(e){return e instanceof B(e).SVGElement}function we(e){return e?xe(e)?e.document:mt(e)?yt(e)?e:Pe(e)||Zt(e)?e.ownerDocument:document:document:document}const Z=nt?c.useLayoutEffect:c.useEffect;function xt(e){const t=c.useRef(e);return Z(()=>{t.current=e}),c.useCallback(function(){for(var n=arguments.length,r=new Array(n),o=0;o<n;o++)r[o]=arguments[o];return t.current==null?void 0:t.current(...r)},[])}function Fn(){const e=c.useRef(null),t=c.useCallback((r,o)=>{e.current=setInterval(r,o)},[]),n=c.useCallback(()=>{e.current!==null&&(clearInterval(e.current),e.current=null)},[]);return[t,n]}function je(e,t){t===void 0&&(t=[e]);const n=c.useRef(e);return Z(()=>{n.current!==e&&(n.current=e)},t),n}function Fe(e,t){const n=c.useRef();return c.useMemo(()=>{const r=e(n.current);return n.current=r,r},[...t])}function Je(e){const t=xt(e),n=c.useRef(null),r=c.useCallback(o=>{o!==n.current&&t?.(o,n.current),n.current=o},[]);return[n,r]}function ht(e){const t=c.useRef();return c.useEffect(()=>{t.current=e},[e]),t.current}let lt={};function Xe(e,t){return c.useMemo(()=>{if(t)return t;const n=lt[e]==null?0:lt[e]+1;return lt[e]=n,e+"-"+n},[e,t])}function en(e){return function(t){for(var n=arguments.length,r=new Array(n>1?n-1:0),o=1;o<n;o++)r[o-1]=arguments[o];return r.reduce((i,s)=>{const a=Object.entries(s);for(const[l,u]of a){const f=i[l];f!=null&&(i[l]=f+e*u)}return i},{...t})}}const ye=en(1),ze=en(-1);function Xn(e){return"clientX"in e&&"clientY"in e}function wt(e){if(!e)return!1;const{KeyboardEvent:t}=B(e.target);return t&&e instanceof t}function Un(e){if(!e)return!1;const{TouchEvent:t}=B(e.target);return t&&e instanceof t}function gt(e){if(Un(e)){if(e.touches&&e.touches.length){const{clientX:t,clientY:n}=e.touches[0];return{x:t,y:n}}else if(e.changedTouches&&e.changedTouches.length){const{clientX:t,clientY:n}=e.changedTouches[0];return{x:t,y:n}}}return Xn(e)?{x:e.clientX,y:e.clientY}:null}const $e=Object.freeze({Translate:{toString(e){if(!e)return;const{x:t,y:n}=e;return"translate3d("+(t?Math.round(t):0)+"px, "+(n?Math.round(n):0)+"px, 0)"}},Scale:{toString(e){if(!e)return;const{scaleX:t,scaleY:n}=e;return"scaleX("+t+") scaleY("+n+")"}},Transform:{toString(e){if(e)return[$e.Translate.toString(e),$e.Scale.toString(e)].join(" ")}},Transition:{toString(e){let{property:t,duration:n,easing:r}=e;return t+" "+n+"ms "+r}}}),zt="a,frame,iframe,input:not([type=hidden]):not(:disabled),select:not(:disabled),textarea:not(:disabled),button:not(:disabled),*[tabindex]";function Yn(e){return e.matches(zt)?e:e.querySelector(zt)}const Wn={display:"none"};function Vn(e){let{id:t,value:n}=e;return $.createElement("div",{id:t,style:Wn},n)}function Hn(e){let{id:t,announcement:n,ariaLiveType:r="assertive"}=e;const o={position:"fixed",top:0,left:0,width:1,height:1,margin:-1,border:0,padding:0,overflow:"hidden",clip:"rect(0 0 0 0)",clipPath:"inset(100%)",whiteSpace:"nowrap"};return $.createElement("div",{id:t,style:o,role:"status","aria-live":r,"aria-atomic":!0},n)}function Kn(){const[e,t]=c.useState("");return{announce:c.useCallback(r=>{r!=null&&t(r)},[]),announcement:e}}const tn=c.createContext(null);function _n(e){const t=c.useContext(tn);c.useEffect(()=>{if(!t)throw new Error("useDndMonitor must be used within a children of <DndContext>");return t(e)},[e,t])}function qn(){const[e]=c.useState(()=>new Set),t=c.useCallback(r=>(e.add(r),()=>e.delete(r)),[e]);return[c.useCallback(r=>{let{type:o,event:i}=r;e.forEach(s=>{var a;return(a=s[o])==null?void 0:a.call(s,i)})},[e]),t]}const Gn={draggable:`
2
2
  To pick up a draggable item, press the space bar.
3
3
  While dragging, use the arrow keys to move the item.
4
4
  Press space again to drop the item in its new position, or press escape to cancel.
@@ -1 +1 @@
1
- import{c as d,d as l,j as e,C as h}from"./index-C3ZSD987.js";const p=[["path",{d:"m9 18 6-6-6-6",key:"mthhwq"}]],g=d("chevron-right",p);const y=[["rect",{width:"14",height:"14",x:"8",y:"8",rx:"2",ry:"2",key:"17jyea"}],["path",{d:"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2",key:"zix9uf"}]],j=d("copy",y);function N({raw:t,parsed:r}){const[s,c]=l.useState(!1);async function a(){try{await navigator.clipboard.writeText(t),c(!0),setTimeout(()=>c(!1),2e3)}catch{}}return e.jsxs("div",{className:"relative w-full overflow-x-auto rounded border border-gray-200 bg-gray-50 p-3 font-mono text-xs leading-relaxed text-gray-800 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-200",children:[e.jsx("button",{type:"button",onClick:a,"aria-label":"Copy JSON",title:s?"Copied":"Copy",className:"absolute right-2 top-2 inline-flex h-6 w-6 items-center justify-center rounded border border-gray-300 bg-white text-gray-600 hover:bg-gray-100 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700",children:s?e.jsx(h,{className:"h-3.5 w-3.5 text-green-600","aria-hidden":!0}):e.jsx(j,{className:"h-3.5 w-3.5","aria-hidden":!0})}),e.jsx(o,{value:r,depth:0})]})}function o({value:t,depth:r}){return t===null?e.jsx("span",{className:"text-purple-600 dark:text-purple-400",children:"null"}):typeof t=="boolean"?e.jsx("span",{className:"text-purple-600 dark:text-purple-400",children:t?"true":"false"}):typeof t=="number"?e.jsx("span",{className:"text-blue-700",children:String(t)}):typeof t=="string"?e.jsxs("span",{className:"text-green-700 dark:text-green-400",children:['"',t,'"']}):Array.isArray(t)?e.jsx(u,{value:t,depth:r}):typeof t=="object"?e.jsx(m,{value:t,depth:r}):e.jsx("span",{className:"text-gray-500",children:String(t)})}function m({value:t,depth:r}){const s=Object.keys(t),[c,a]=l.useState(r<2);return s.length===0?e.jsx("span",{className:"text-gray-500",children:"{}"}):e.jsx(x,{open:c,onToggle:()=>a(n=>!n),collapsedLabel:`{…} ${s.length} ${s.length===1?"key":"keys"}`,openBracket:"{",closeBracket:"}",depth:r,children:s.map((n,i)=>e.jsxs("div",{className:"pl-4",children:[e.jsxs("span",{className:"text-rose-700 dark:text-rose-400",children:['"',n,'"']}),e.jsx("span",{className:"text-gray-500",children:": "}),e.jsx(o,{value:t[n],depth:r+1}),i<s.length-1?e.jsx("span",{className:"text-gray-500",children:","}):null]},n))})}function u({value:t,depth:r}){const[s,c]=l.useState(r<2);return t.length===0?e.jsx("span",{className:"text-gray-500",children:"[]"}):e.jsx(x,{open:s,onToggle:()=>c(a=>!a),collapsedLabel:`[…] ${t.length} ${t.length===1?"item":"items"}`,openBracket:"[",closeBracket:"]",depth:r,children:t.map((a,n)=>e.jsxs("div",{className:"pl-4",children:[e.jsx(o,{value:a,depth:r+1}),n<t.length-1?e.jsx("span",{className:"text-gray-500",children:","}):null]},n))})}function x({open:t,onToggle:r,collapsedLabel:s,openBracket:c,closeBracket:a,depth:n,children:i}){return e.jsxs("span",{children:[e.jsx("button",{type:"button",onClick:r,"aria-expanded":t,className:"inline-flex items-center align-baseline text-gray-500 hover:text-gray-800 dark:hover:text-gray-200",children:e.jsx(g,{className:`h-3 w-3 shrink-0 transition-transform ${t?"rotate-90":""}`,"aria-hidden":!0})}),e.jsx("span",{className:"text-gray-500",children:c}),t?e.jsxs(e.Fragment,{children:[i,e.jsx("div",{className:n===0?"":"pl-0",children:e.jsx("span",{className:"text-gray-500",children:a})})]}):e.jsxs(e.Fragment,{children:[e.jsx("span",{className:"px-1 text-gray-500",children:s}),e.jsx("span",{className:"text-gray-500",children:a})]})]})}export{N as default};
1
+ import{c as d,d as l,j as e,C as h}from"./index-CGh8t5dj.js";const p=[["path",{d:"m9 18 6-6-6-6",key:"mthhwq"}]],g=d("chevron-right",p);const y=[["rect",{width:"14",height:"14",x:"8",y:"8",rx:"2",ry:"2",key:"17jyea"}],["path",{d:"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2",key:"zix9uf"}]],j=d("copy",y);function N({raw:t,parsed:r}){const[s,c]=l.useState(!1);async function a(){try{await navigator.clipboard.writeText(t),c(!0),setTimeout(()=>c(!1),2e3)}catch{}}return e.jsxs("div",{className:"relative w-full overflow-x-auto rounded border border-gray-200 bg-gray-50 p-3 font-mono text-xs leading-relaxed text-gray-800 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-200",children:[e.jsx("button",{type:"button",onClick:a,"aria-label":"Copy JSON",title:s?"Copied":"Copy",className:"absolute right-2 top-2 inline-flex h-6 w-6 items-center justify-center rounded border border-gray-300 bg-white text-gray-600 hover:bg-gray-100 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700",children:s?e.jsx(h,{className:"h-3.5 w-3.5 text-green-600","aria-hidden":!0}):e.jsx(j,{className:"h-3.5 w-3.5","aria-hidden":!0})}),e.jsx(o,{value:r,depth:0})]})}function o({value:t,depth:r}){return t===null?e.jsx("span",{className:"text-purple-600 dark:text-purple-400",children:"null"}):typeof t=="boolean"?e.jsx("span",{className:"text-purple-600 dark:text-purple-400",children:t?"true":"false"}):typeof t=="number"?e.jsx("span",{className:"text-blue-700",children:String(t)}):typeof t=="string"?e.jsxs("span",{className:"text-green-700 dark:text-green-400",children:['"',t,'"']}):Array.isArray(t)?e.jsx(u,{value:t,depth:r}):typeof t=="object"?e.jsx(m,{value:t,depth:r}):e.jsx("span",{className:"text-gray-500",children:String(t)})}function m({value:t,depth:r}){const s=Object.keys(t),[c,a]=l.useState(r<2);return s.length===0?e.jsx("span",{className:"text-gray-500",children:"{}"}):e.jsx(x,{open:c,onToggle:()=>a(n=>!n),collapsedLabel:`{…} ${s.length} ${s.length===1?"key":"keys"}`,openBracket:"{",closeBracket:"}",depth:r,children:s.map((n,i)=>e.jsxs("div",{className:"pl-4",children:[e.jsxs("span",{className:"text-rose-700 dark:text-rose-400",children:['"',n,'"']}),e.jsx("span",{className:"text-gray-500",children:": "}),e.jsx(o,{value:t[n],depth:r+1}),i<s.length-1?e.jsx("span",{className:"text-gray-500",children:","}):null]},n))})}function u({value:t,depth:r}){const[s,c]=l.useState(r<2);return t.length===0?e.jsx("span",{className:"text-gray-500",children:"[]"}):e.jsx(x,{open:s,onToggle:()=>c(a=>!a),collapsedLabel:`[…] ${t.length} ${t.length===1?"item":"items"}`,openBracket:"[",closeBracket:"]",depth:r,children:t.map((a,n)=>e.jsxs("div",{className:"pl-4",children:[e.jsx(o,{value:a,depth:r+1}),n<t.length-1?e.jsx("span",{className:"text-gray-500",children:","}):null]},n))})}function x({open:t,onToggle:r,collapsedLabel:s,openBracket:c,closeBracket:a,depth:n,children:i}){return e.jsxs("span",{children:[e.jsx("button",{type:"button",onClick:r,"aria-expanded":t,className:"inline-flex items-center align-baseline text-gray-500 hover:text-gray-800 dark:hover:text-gray-200",children:e.jsx(g,{className:`h-3 w-3 shrink-0 transition-transform ${t?"rotate-90":""}`,"aria-hidden":!0})}),e.jsx("span",{className:"text-gray-500",children:c}),t?e.jsxs(e.Fragment,{children:[i,e.jsx("div",{className:n===0?"":"pl-0",children:e.jsx("span",{className:"text-gray-500",children:a})})]}):e.jsxs(e.Fragment,{children:[e.jsx("span",{className:"px-1 text-gray-500",children:s}),e.jsx("span",{className:"text-gray-500",children:a})]})]})}export{N as default};