radiant-docs 0.1.7 → 0.1.9

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 (78) hide show
  1. package/dist/index.js +28 -5
  2. package/package.json +5 -4
  3. package/template/astro.config.mjs +76 -3
  4. package/template/package-lock.json +924 -737
  5. package/template/package.json +7 -5
  6. package/template/scripts/generate-og-images.mjs +335 -0
  7. package/template/scripts/generate-og-metadata.mjs +173 -0
  8. package/template/scripts/rewrite-static-asset-host.mjs +408 -0
  9. package/template/scripts/stamp-image-versions.mjs +277 -0
  10. package/template/scripts/stamp-og-image-versions.mjs +199 -0
  11. package/template/scripts/stamp-pagefind-runtime-version.mjs +140 -0
  12. package/template/src/assets/fonts/geist-mono/cyrillic.woff2 +0 -0
  13. package/template/src/assets/fonts/geist-mono/latin-ext.woff2 +0 -0
  14. package/template/src/assets/fonts/geist-mono/latin.woff2 +0 -0
  15. package/template/src/assets/fonts/google-sans-flex/canadian-aboriginal.woff2 +0 -0
  16. package/template/src/assets/fonts/google-sans-flex/cherokee.woff2 +0 -0
  17. package/template/src/assets/fonts/google-sans-flex/latin-ext.woff2 +0 -0
  18. package/template/src/assets/fonts/google-sans-flex/latin.woff2 +0 -0
  19. package/template/src/assets/fonts/google-sans-flex/math.woff2 +0 -0
  20. package/template/src/assets/fonts/google-sans-flex/nushu.woff2 +0 -0
  21. package/template/src/assets/fonts/google-sans-flex/symbols.woff2 +0 -0
  22. package/template/src/assets/fonts/google-sans-flex/syriac.woff2 +0 -0
  23. package/template/src/assets/fonts/google-sans-flex/tifinagh.woff2 +0 -0
  24. package/template/src/assets/fonts/google-sans-flex/vietnamese.woff2 +0 -0
  25. package/template/src/components/Footer.astro +94 -0
  26. package/template/src/components/Header.astro +11 -66
  27. package/template/src/components/LogoLink.astro +103 -0
  28. package/template/src/components/MdxPage.astro +126 -11
  29. package/template/src/components/OpenApiPage.astro +1036 -69
  30. package/template/src/components/Search.astro +0 -2
  31. package/template/src/components/SidebarDropdown.astro +34 -14
  32. package/template/src/components/SidebarGroup.astro +3 -6
  33. package/template/src/components/SidebarLink.astro +22 -12
  34. package/template/src/components/SidebarMenu.astro +19 -16
  35. package/template/src/components/SidebarSegmented.astro +99 -0
  36. package/template/src/components/SidebarSubgroup.astro +12 -12
  37. package/template/src/components/ThemeSwitcher.astro +30 -7
  38. package/template/src/components/endpoint/PlaygroundBar.astro +32 -36
  39. package/template/src/components/endpoint/PlaygroundButton.astro +40 -4
  40. package/template/src/components/endpoint/PlaygroundField.astro +1068 -22
  41. package/template/src/components/endpoint/PlaygroundForm.astro +559 -61
  42. package/template/src/components/endpoint/RequestSnippets.astro +342 -193
  43. package/template/src/components/endpoint/ResponseDisplay.astro +161 -147
  44. package/template/src/components/endpoint/ResponseFieldTree.astro +134 -0
  45. package/template/src/components/endpoint/ResponseFields.astro +711 -68
  46. package/template/src/components/endpoint/ResponseSnippets.astro +299 -173
  47. package/template/src/components/sidebar/SidebarEndpointLink.astro +1 -1
  48. package/template/src/components/ui/CodeLanguageIcon.astro +19 -0
  49. package/template/src/components/ui/CodeTabEdge.astro +79 -0
  50. package/template/src/components/ui/Field.astro +103 -20
  51. package/template/src/components/ui/Icon.astro +32 -0
  52. package/template/src/components/ui/ListChevronsToggle.astro +31 -0
  53. package/template/src/components/ui/Tag.astro +1 -1
  54. package/template/src/components/user/{Accordian.astro → Accordion.astro} +6 -6
  55. package/template/src/components/user/Callout.astro +5 -9
  56. package/template/src/components/user/CodeBlock.astro +400 -0
  57. package/template/src/components/user/CodeGroup.astro +225 -0
  58. package/template/src/components/user/ComponentPreview.astro +1 -0
  59. package/template/src/components/user/ComponentPreviewBlock.astro +181 -0
  60. package/template/src/components/user/Image.astro +132 -0
  61. package/template/src/components/user/Steps.astro +1 -3
  62. package/template/src/components/user/Tabs.astro +2 -2
  63. package/template/src/content.config.ts +1 -0
  64. package/template/src/layouts/Layout.astro +109 -8
  65. package/template/src/lib/code/code-block.ts +546 -0
  66. package/template/src/lib/frontmatter-schema.ts +8 -7
  67. package/template/src/lib/mdx/remark-code-block-component.ts +342 -0
  68. package/template/src/lib/mdx/remark-demote-h1.ts +16 -0
  69. package/template/src/lib/pagefind.ts +19 -5
  70. package/template/src/lib/routes.ts +49 -31
  71. package/template/src/lib/utils.ts +20 -0
  72. package/template/src/lib/validation.ts +638 -200
  73. package/template/src/pages/[...slug].astro +18 -5
  74. package/template/src/styles/geist-mono.css +33 -0
  75. package/template/src/styles/global.css +89 -84
  76. package/template/src/styles/google-sans-flex.css +143 -0
  77. package/template/ec.config.mjs +0 -51
  78. /package/template/src/components/user/{AccordianGroup.astro → AccordionGroup.astro} +0 -0
@@ -1,175 +1,189 @@
1
1
  ---
2
+ import CodeTabEdge from "../ui/CodeTabEdge.astro";
2
3
  import { Icon } from "astro-icon/components";
3
4
  ---
4
5
 
5
6
  <div
6
7
  x-data={`{
8
+ tabs: [
9
+ { id: 'body', label: 'Body' },
10
+ { id: 'header', label: 'Header' }
11
+ ],
7
12
  selected: 'body',
13
+ pillLeft: 0,
14
+ pillWidth: 0,
15
+ pillVisible: false,
16
+ tabSyncHandler: null,
17
+ playgroundOpenHandler: null,
18
+ init() {
19
+ const sync = () => this.syncPill();
20
+ this.tabSyncHandler = sync;
21
+ const onPlaygroundOpen = () => {
22
+ this.$nextTick(() => this.syncPill());
23
+ };
24
+ this.playgroundOpenHandler = onPlaygroundOpen;
25
+ this.$nextTick(() => {
26
+ this.syncPill();
27
+ this.$refs.tabList?.addEventListener('scroll', sync, {
28
+ passive: true
29
+ });
30
+ window.addEventListener('resize', sync);
31
+ window.addEventListener('rd:playground-opened', onPlaygroundOpen);
32
+ });
33
+ },
34
+ destroy() {
35
+ if (!this.tabSyncHandler) return;
36
+ this.$refs.tabList?.removeEventListener('scroll', this.tabSyncHandler);
37
+ window.removeEventListener('resize', this.tabSyncHandler);
38
+ if (this.playgroundOpenHandler) {
39
+ window.removeEventListener('rd:playground-opened', this.playgroundOpenHandler);
40
+ }
41
+ this.tabSyncHandler = null;
42
+ this.playgroundOpenHandler = null;
43
+ },
44
+ syncPill() {
45
+ const tabs = this.$refs.tabList;
46
+ if (!tabs) return;
47
+ const activeTab = tabs.querySelector(
48
+ '[data-rd-response-tab=\"' + this.selected + '\"]'
49
+ );
50
+ if (!activeTab) {
51
+ this.pillVisible = false;
52
+ return;
53
+ }
54
+ const tabsRect = tabs.getBoundingClientRect();
55
+ const activeRect = activeTab.getBoundingClientRect();
56
+ this.pillLeft = activeRect.left - tabsRect.left + tabs.scrollLeft;
57
+ this.pillWidth = activeRect.width;
58
+ this.pillVisible = true;
59
+ },
8
60
  select(value) {
9
61
  this.selected = value;
10
- this.$refs.button?.focus();
11
- this.close();
62
+ this.$nextTick(() => {
63
+ this.syncPill();
64
+ window.dispatchEvent(new CustomEvent('rd:snippet-content-change'));
65
+ });
12
66
  },
13
- close(focusAfter) {
14
- if (!this.open) return;
15
- this.open = false;
16
- focusAfter?.focus();
17
- }
18
67
  }`}
19
- class="bg-neutral-100 rounded-[14px] border border-neutral-200 p-[3px] inset-shadow-xs"
68
+ class="h-full min-h-0 w-full min-w-0"
20
69
  x-bind:class="loading && 'animate-loading'"
21
70
  >
22
- <div class="flex justify-between items-center">
71
+ <div
72
+ class="group/prose-code not-prose relative h-full min-h-0 w-full max-w-full min-w-0"
73
+ >
23
74
  <div
24
- class="flex-1 min-w-0 flex items-center font-medium ml-2 text-neutral-700 text-sm"
75
+ class="flex h-full min-h-0 w-full max-w-full min-w-0 flex-col overflow-hidden rounded-xl border border-neutral-200 bg-white shadow-xs"
25
76
  >
26
- <span
27
- class:list={[
28
- "shrink-0 text-xs uppercase font-semibold px-1.5 border py-px rounded-md mr-1.5",
29
- ]}
30
- x-bind:class="{
31
- 'bg-green-50 text-green-700/70 border-green-700/10': Math.floor(response?.status / 100) === 2,
32
- 'bg-blue-50 text-blue-700/70 border-blue-700/10': Math.floor(response?.status / 100) === 3,
33
- 'bg-amber-50 text-amber-600/80 border-amber-700/10': Math.floor(response?.status / 100) === 4,
34
- 'bg-red-50 text-red-700/70 border-red-700/10': Math.floor(response?.status / 100) === 5
35
- }"
36
- x-text="response?.status"
37
- x-show="response?.status"></span>
38
- <span class="truncate" x-text="response?.statusText"></span>
39
- </div>
40
- <div
41
- x-data="{
42
- open: false,
43
- toggle() {
44
- if (this.open) {
45
- return this.close()
46
- }
47
-
48
- this.$refs.button.focus()
49
-
50
- this.open = true
51
- },
52
- close(focusAfter) {
53
- if (! this.open) return
54
-
55
- this.open = false
56
-
57
- focusAfter && focusAfter.focus()
58
- }
59
- }"
60
- x-on:keydown.escape.prevent.stop="close($refs.button)"
61
- x-on:focusin.window="! $refs.panel.contains($event.target) && close()"
62
- x-id="['dropdown-button']"
63
- class="shrink-0 relative max-w-24 w-full"
64
- >
65
- <button
66
- x-ref="button"
67
- x-on:click="toggle()"
68
- :aria-expanded="open"
69
- :aria-controls="$id('dropdown-button')"
70
- type="button"
71
- class="flex items-center justify-between px-3 pt-2 pb-1.5 relative border-x border-t border-neutral-200 rounded-t-xl w-full text-sm font-medium bg-white shadow-xs cursor-pointer after:absolute after:-bottom-[2px] after:inset-x-0 after:z-50 after:h-[3px] after:bg-white"
77
+ <div
78
+ class="flex items-center justify-between gap-2 border-b border-neutral-200 rounded-t-xl bg-neutral-50 inset-shadow-sm inset-shadow-neutral-100/80"
72
79
  >
73
- <span class="flex items-center gap-2">
74
- <span x-text="selected" class="capitalize"></span>
75
- </span>
76
- <Icon name="lucide:chevrons-up-down" />
77
- </button>
78
- <!-- Panel -->
79
- <ul
80
- x-ref="panel"
81
- x-show="open"
82
- x-transition.origin.top.left
83
- x-on:click.outside="close($refs.button)"
84
- :id="$id('dropdown-button')"
85
- x-cloak
86
- role="menu"
87
- class="absolute top-full right-2 min-w-28 rounded-lg shadow-xl mt-2 z-10 origin-top-right bg-white py-0.5 outline-none border border-neutral-200"
88
- >
89
- <li role="menuitem">
90
- <button
91
- x-on:click="select('header')"
92
- type="button"
93
- class="w-full text-left px-3 py-2 text-sm text-neutral-700 rounded-md transition-colors flex items-center gap-2 relative before:absolute before:inset-x-1 before:inset-y-0.5 before:-z-10 before:rounded-md before:duration-150 cursor-pointer"
94
- x-bind:class="selected === 'header' ? 'before:bg-neutral-200/50 text-neutral-900' : 'hover:before:bg-neutral-100/70'"
95
- >
96
- Header
97
- </button>
98
- </li>
99
- <li role="menuitem">
100
- <button
101
- x-on:click="select('body')"
102
- type="button"
103
- class="w-full text-left px-3 py-2 text-sm text-neutral-700 rounded-md transition-colors flex items-center gap-2 relative before:absolute before:inset-x-1 before:inset-y-0.5 before:-z-10 before:rounded-md before:duration-150 cursor-pointer"
104
- x-bind:class="selected === 'body' ? 'before:bg-neutral-200/50 text-neutral-900' : 'hover:before:bg-neutral-100/70'"
80
+ <div class="min-w-0 flex-1 overflow-hidden rounded-t-xl">
81
+ <div
82
+ x-ref="tabList"
83
+ class="relative flex min-w-0 items-end gap-1 overflow-x-auto px-1 pr-8 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
105
84
  >
106
- Body
107
- </button>
108
- </li>
109
- </ul>
110
- </div>
111
- </div>
112
- <div
113
- class="bg-white border border-neutral-200 rounded-xl rounded-tr-none shadow-xs transition duration-300"
114
- >
115
- <div x-bind:class="loading && 'opacity-60 duration-700'">
116
- <div x-show="selected === 'body'">
117
- <div
118
- x-show="!response || !response.highlightedData"
119
- class="text-sm px-4 py-4 xs:py-8 flex flex-col items-center justify-center"
120
- >
121
- <div class="bg-neutral-50 p-2 rounded-xl mb-1">
122
- <Icon
123
- class="size-6 text-neutral-300"
124
- name="lucide:square-arrow-up-right"
125
- />
85
+ <div
86
+ aria-hidden="true"
87
+ class="pointer-events-none absolute top-1/2 z-0 h-[28px] -translate-y-1/2 rounded-lg border-[0.5px] border-neutral-200 bg-white shadow-xs transition-[left,width,opacity] duration-200 ease-out"
88
+ x-bind:class="pillVisible ? 'opacity-100' : 'opacity-0'"
89
+ x-bind:style="'left:' + pillLeft + 'px;width:' + pillWidth + 'px;'"
90
+ >
91
+ </div>
92
+
93
+ <template x-for="tab in tabs" :key="tab.id">
94
+ <button
95
+ type="button"
96
+ x-bind:data-rd-response-tab="tab.id"
97
+ x-on:click="select(tab.id)"
98
+ class="relative z-10 inline-flex h-9 items-center gap-2 border-0 bg-transparent px-3 py-1.5 text-xs font-medium transition-colors duration-150 focus:outline-none focus-visible:outline-none cursor-pointer"
99
+ x-bind:class="selected === tab.id ? 'text-neutral-900' : 'text-neutral-600'"
100
+ >
101
+ <span class="whitespace-pre leading-none" x-text="tab.label"
102
+ ></span>
103
+ </button>
104
+ </template>
126
105
  </div>
127
- <div class="text-lg text-center text-neutral-700">Send request</div>
128
- <p class="text-sm text-neutral-500 text-center">
129
- Send a request to see the response body.
130
- </p>
131
- </div>
132
- <div x-show="response && response.highlightedData">
133
- <pre
134
- class="bg-transparent! m-0!"><code class="language-json text-[13px]! font-mono! text-neutral-700!" x-html="response?.highlightedData" /></pre>
135
106
  </div>
136
- </div>
137
- <div x-show="selected === 'header'">
107
+
138
108
  <div
139
- x-show="!response || !response.headers || Object.keys(response.headers).length === 0"
140
- class="text-sm px-4 py-4 xs:py-8 flex flex-col items-center justify-center"
109
+ class="relative h-9 w-fit max-w-full shrink-0 rounded-tr-xl bg-white"
141
110
  >
142
- <div class="bg-neutral-50 p-2 rounded-xl mb-1">
143
- <Icon
144
- class="size-6 text-neutral-300"
145
- name="lucide:square-arrow-up-right"
146
- />
111
+ <div class="absolute inset-x-0 -bottom-px h-px bg-white"></div>
112
+ <CodeTabEdge
113
+ className="pointer-events-none absolute -top-px right-full z-10 h-[calc(100%+2px)] rotate-y-180"
114
+ />
115
+ <div
116
+ class="relative z-20 inline-flex h-9 max-w-full items-center gap-1.5 w-14 pr-5 py-1.5 text-xs font-medium"
117
+ >
118
+ <span
119
+ class="size-[7px] shrink-0 rounded-full"
120
+ x-bind:class="{
121
+ 'bg-green-700/70': Math.floor((response?.status || 0) / 100) === 2,
122
+ 'bg-blue-700/70': Math.floor((response?.status || 0) / 100) === 3,
123
+ 'bg-yellow-500/80': Math.floor((response?.status || 0) / 100) === 4,
124
+ 'bg-red-700/70': Math.floor((response?.status || 0) / 100) === 5,
125
+ 'bg-neutral-300': !response?.status || Math.floor((response?.status || 0) / 100) < 2
126
+ }"
127
+ ></span>
128
+ <span
129
+ class="leading-none"
130
+ x-bind:class="response?.status ? 'text-neutral-700' : 'text-neutral-300'"
131
+ x-text="response?.status ?? '---'"></span>
147
132
  </div>
148
- <div class="text-lg text-center text-neutral-700">Send request</div>
149
- <p class="text-sm text-neutral-500 text-center">
150
- Send a request to see the response header.
151
- </p>
152
133
  </div>
134
+ </div>
135
+
136
+ <div class="relative min-h-0 min-w-0 flex-1 overflow-hidden rounded-b-xl">
153
137
  <div
154
- x-show="response && response.headers && Object.keys(response.headers).length > 0"
155
- class=""
138
+ class="relative h-full overflow-auto [scrollbar-width:thin] [scrollbar-color:var(--color-neutral-300)_transparent] [&::-webkit-scrollbar]:h-1.5 [&::-webkit-scrollbar]:w-1.5 [&::-webkit-scrollbar-track]:bg-transparent [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:bg-neutral-300/70 hover:[&::-webkit-scrollbar-thumb]:bg-neutral-300/90"
156
139
  >
157
- <table class="w-full border-collapse text-xs table-fixed">
158
- <tbody>
159
- <template x-for="(value, key) in response?.headers" :key="key">
160
- <tr
161
- class="flex border-b last:border-b-0 divide-x divide-neutral-100 border-neutral-100 hover:bg-neutral-50/50"
162
- >
163
- <td
164
- class="py-2 px-3 basis-1/2 font-mono font-medium text-neutral-700 overflow-x-auto whitespace-nowrap"
165
- x-text="key"></td>
166
- <td
167
- class="py-2 px-3 basis-1/2 font-mono text-neutral-600 overflow-x-auto whitespace-nowrap"
168
- x-text="value"></td>
169
- </tr>
170
- </template>
171
- </tbody>
172
- </table>
140
+ <div
141
+ class="pointer-events-none sticky top-0 z-10 -mb-4 h-4 w-full bg-linear-to-b from-white to-transparent"
142
+ >
143
+ </div>
144
+ <div x-show="selected === 'body'" class="h-full">
145
+ <div
146
+ x-show="!response || !response.highlightedData"
147
+ class="text-sm px-4 py-4 xs:py-8 flex flex-col items-center justify-center h-full"
148
+ >
149
+ <div class="bg-neutral-50 p-2 rounded-xl mb-1">
150
+ <Icon
151
+ class="size-6 text-neutral-300"
152
+ name="lucide:square-arrow-up-right"
153
+ />
154
+ </div>
155
+ <div class="text-lg font-medium text-center">Send request</div>
156
+ <p class="text-sm text-neutral-500 text-center">
157
+ Send a request to see the response body.
158
+ </p>
159
+ </div>
160
+ <div x-show="response && response.highlightedData">
161
+ <pre
162
+ class="relative m-0 min-w-full bg-white p-0 text-[13px] leading-6"><code class="block min-w-full px-4 py-2.5 font-mono text-neutral-800" x-html="response?.highlightedData" /></pre>
163
+ </div>
164
+ </div>
165
+
166
+ <div x-show="selected === 'header'" x-cloak class="h-full">
167
+ <div
168
+ x-show="!response || !response.highlightedHeaders"
169
+ class="text-sm px-4 py-4 xs:py-8 flex flex-col items-center justify-center h-full"
170
+ >
171
+ <div class="bg-neutral-50 p-2 rounded-xl mb-1">
172
+ <Icon
173
+ class="size-6 text-neutral-300"
174
+ name="lucide:square-arrow-up-right"
175
+ />
176
+ </div>
177
+ <div class="text-lg font-medium text-center">Send request</div>
178
+ <p class="text-sm text-neutral-500 text-center">
179
+ Send a request to see the response header.
180
+ </p>
181
+ </div>
182
+ <div x-show="response && response.highlightedHeaders">
183
+ <pre
184
+ class="relative m-0 min-w-full bg-white p-0 text-[13px] leading-6"><code class="block min-w-full px-4 py-2.5 font-mono text-neutral-800" x-html="response?.highlightedHeaders" /></pre>
185
+ </div>
186
+ </div>
173
187
  </div>
174
188
  </div>
175
189
  </div>
@@ -0,0 +1,134 @@
1
+ ---
2
+ import Field from "../ui/Field.astro";
3
+ import ListChevronsToggle from "../ui/ListChevronsToggle.astro";
4
+
5
+ interface ResponseFieldVariant {
6
+ label: string;
7
+ fields: ResponseField[];
8
+ }
9
+
10
+ interface ResponseField {
11
+ name: string;
12
+ required: boolean;
13
+ type: string;
14
+ description: string;
15
+ nested?: ResponseField[];
16
+ enum?: (string | number)[];
17
+ minLength?: number;
18
+ maxLength?: number;
19
+ minimum?: number;
20
+ maximum?: number;
21
+ exclusiveMinimum?: number;
22
+ exclusiveMaximum?: number;
23
+ variants?: ResponseFieldVariant[];
24
+ variantType?: "oneOf" | "anyOf";
25
+ }
26
+
27
+ interface Props {
28
+ fields: ResponseField[];
29
+ depth?: number;
30
+ }
31
+
32
+ const { fields, depth = 0 } = Astro.props;
33
+
34
+ function getDisplayName(name: string, depth: number): string {
35
+ if (depth <= 0 || !name.includes(".")) return name;
36
+ const segments = name.split(".");
37
+ return segments[segments.length - 1] || name;
38
+ }
39
+
40
+ function countImmediateChildren(field: ResponseField): number {
41
+ const nestedCount = field.nested ? field.nested.length : 0;
42
+ const variantCount = field.variants
43
+ ? field.variants.reduce((sum, variant) => sum + variant.fields.length, 0)
44
+ : 0;
45
+ return nestedCount + variantCount;
46
+ }
47
+ ---
48
+
49
+ <div class="flex flex-col gap-4 divide-y divide-neutral-100 *:pb-3 *:last:pb-0">
50
+ {
51
+ fields.map((field) => {
52
+ const revealedFieldCount = countImmediateChildren(field);
53
+ const nestedFieldCount = field.nested ? field.nested.length : 0;
54
+ const hasExpandableContent = revealedFieldCount > 0;
55
+
56
+ return (
57
+ <div x-data="{ expanded: false }">
58
+ <Field
59
+ name={getDisplayName(field.name, depth)}
60
+ type={field.type}
61
+ required={field.required}
62
+ description={field.description}
63
+ enum={field.enum}
64
+ minLength={field.minLength}
65
+ maxLength={field.maxLength}
66
+ minimum={field.minimum}
67
+ maximum={field.maximum}
68
+ exclusiveMinimum={field.exclusiveMinimum}
69
+ exclusiveMaximum={field.exclusiveMaximum}
70
+ />
71
+ {hasExpandableContent && (
72
+ <div
73
+ class="mt-2 w-full overflow-hidden rounded-lg border border-neutral-200 bg-white transition-colors duration-200"
74
+ x-bind:class="expanded ? 'border-neutral-300' : 'border-neutral-200'"
75
+ >
76
+ <button
77
+ type="button"
78
+ x-on:click="expanded = !expanded"
79
+ x-bind:aria-expanded="expanded"
80
+ class="group flex w-full items-center justify-between gap-3 px-3 py-2 text-left text-xs font-medium text-neutral-700 hover:bg-neutral-50 cursor-pointer transition-colors duration-200"
81
+ >
82
+ <span class="inline-flex items-center gap-2">
83
+ <ListChevronsToggle class="size-4 shrink-0 text-neutral-400 group-hover:text-neutral-600 transition duration-200" />
84
+ <span>
85
+ {revealedFieldCount}{" "}
86
+ {revealedFieldCount === 1
87
+ ? "field"
88
+ : "fields"}
89
+ </span>
90
+ </span>
91
+ </button>
92
+ <div x-show="expanded" x-collapse x-cloak>
93
+ <div class="border-t border-neutral-100 px-3 py-3">
94
+ {field.nested && field.nested.length > 0 && nestedFieldCount > 0 && (
95
+ <Astro.self fields={field.nested} depth={depth + 1} />
96
+ )}
97
+ {field.variants && field.variants.length > 0 && (
98
+ <div class:list={["space-y-2", nestedFieldCount > 0 && "mt-3"]}>
99
+ <p class="text-xs text-neutral-500">
100
+ {field.variantType === "anyOf"
101
+ ? "One or more variants may apply."
102
+ : "One of these variants applies."}
103
+ </p>
104
+ {field.variants.map((variant, index) => (
105
+ <>
106
+ <div class="rounded-lg border border-neutral-200 bg-white p-3">
107
+ <div class="mb-2 text-xs font-medium text-neutral-600">
108
+ {variant.label}
109
+ </div>
110
+ <Astro.self fields={variant.fields} depth={depth + 1} />
111
+ </div>
112
+ {field.variantType === "oneOf" &&
113
+ index < field.variants.length - 1 && (
114
+ <div class="flex items-center gap-2 py-0">
115
+ <div class="h-px flex-1 bg-neutral-200" />
116
+ <span class="px-1 text-[10px] uppercase tracking-wide text-neutral-500">
117
+ OR
118
+ </span>
119
+ <div class="h-px flex-1 bg-neutral-200" />
120
+ </div>
121
+ )}
122
+ </>
123
+ ))}
124
+ </div>
125
+ )}
126
+ </div>
127
+ </div>
128
+ </div>
129
+ )}
130
+ </div>
131
+ );
132
+ })
133
+ }
134
+ </div>