astro-tractstack 2.3.1 → 2.3.3

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 (73) hide show
  1. package/bin/create-tractstack.js +3 -3
  2. package/dist/index.js +69 -11
  3. package/package.json +1 -1
  4. package/templates/custom/shopify/Cart.tsx +99 -19
  5. package/templates/custom/shopify/CheckoutModal.tsx +196 -10
  6. package/templates/custom/shopify/ShopifyCartManager.tsx +79 -76
  7. package/templates/custom/shopify/ShopifyCheckout.tsx +4 -33
  8. package/templates/custom/shopify/ShopifyProductGrid.tsx +42 -14
  9. package/templates/custom/shopify/ShopifyServiceList.tsx +94 -50
  10. package/templates/custom/shopify/cart.astro +7 -1
  11. package/templates/src/components/Footer.astro +2 -2
  12. package/templates/src/components/Header.astro +17 -9
  13. package/templates/src/components/Menu.tsx +157 -135
  14. package/templates/src/components/codehooks/BunnyVideoSetup.tsx +2 -2
  15. package/templates/src/components/codehooks/EpinetDurationSelector.tsx +27 -6
  16. package/templates/src/components/codehooks/EpinetTableView.tsx +153 -112
  17. package/templates/src/components/codehooks/EpinetWrapper.tsx +4 -1
  18. package/templates/src/components/codehooks/FeaturedArticleSetup.tsx +8 -1
  19. package/templates/src/components/codehooks/ProductCardSetup.tsx +9 -1
  20. package/templates/src/components/codehooks/ProductGridSetup.tsx +9 -1
  21. package/templates/src/components/compositor/nodes/BgPaneWrapper.tsx +2 -1
  22. package/templates/src/components/compositor/nodes/GhostInsertBlock.tsx +1 -1
  23. package/templates/src/components/edit/ToolBar.tsx +2 -1
  24. package/templates/src/components/edit/context/ContextPaneConfig_slug.tsx +2 -2
  25. package/templates/src/components/edit/pane/AddPanePanel_codehook.tsx +13 -0
  26. package/templates/src/components/edit/pane/AddPanePanel_newCustomCopy.tsx +2 -2
  27. package/templates/src/components/edit/pane/ConfigPanePanel.tsx +1 -1
  28. package/templates/src/components/edit/state/SaveModal.tsx +1 -1
  29. package/templates/src/components/edit/widgets/InteractiveDisclosureWidget.tsx +8 -3
  30. package/templates/src/components/form/DateTimeInput.tsx +10 -3
  31. package/templates/src/components/form/FileUpload.tsx +11 -5
  32. package/templates/src/components/form/NumberInput.tsx +2 -2
  33. package/templates/src/components/form/advanced/APIConfigSection.tsx +221 -39
  34. package/templates/src/components/form/brand/SiteConfigSection.tsx +10 -0
  35. package/templates/src/components/form/shopify/SchedulingSection.tsx +44 -0
  36. package/templates/src/components/storykeep/Dashboard_Advanced.tsx +10 -1
  37. package/templates/src/components/storykeep/Dashboard_Shopify.tsx +16 -8
  38. package/templates/src/components/storykeep/controls/content/BeliefForm.tsx +2 -2
  39. package/templates/src/components/storykeep/controls/content/ManageContent.tsx +1 -0
  40. package/templates/src/components/storykeep/controls/content/ProductTable.tsx +2 -2
  41. package/templates/src/components/storykeep/controls/content/ResourceBulkIngest.tsx +79 -51
  42. package/templates/src/components/storykeep/controls/content/ResourceForm.tsx +80 -0
  43. package/templates/src/components/storykeep/controls/content/ResourceTable.tsx +1 -0
  44. package/templates/src/components/storykeep/email-builder/Blocks.tsx +169 -0
  45. package/templates/src/components/storykeep/email-builder/EmailBuilder.tsx +223 -0
  46. package/templates/src/components/storykeep/email-builder/PreviewModal.tsx +136 -0
  47. package/templates/src/components/storykeep/email-builder/PropertyPanel.tsx +154 -0
  48. package/templates/src/components/storykeep/shopify/ShopifyDashboard.tsx +1 -8
  49. package/templates/src/components/storykeep/shopify/ShopifyDashboard_Bookings.tsx +118 -14
  50. package/templates/src/components/storykeep/shopify/ShopifyDashboard_Emails.tsx +105 -0
  51. package/templates/src/constants.ts +2 -0
  52. package/templates/src/layouts/Layout.astro +8 -5
  53. package/templates/src/pages/api/google/oauth/callback.ts +50 -0
  54. package/templates/src/pages/api/google/oauth/disconnect.ts +32 -0
  55. package/templates/src/pages/api/google/oauth/start.ts +32 -0
  56. package/templates/src/pages/api/google/oauth/status.ts +32 -0
  57. package/templates/src/pages/privacy.astro +84 -0
  58. package/templates/src/pages/terms.astro +47 -0
  59. package/templates/src/stores/shopify.ts +21 -0
  60. package/templates/src/types/formTypes.ts +4 -2
  61. package/templates/src/types/tractstack.ts +35 -2
  62. package/templates/src/utils/api/advancedHelpers.ts +16 -0
  63. package/templates/src/utils/api/bookingHelpers.ts +3 -1
  64. package/templates/src/utils/api/brandConfig.ts +2 -0
  65. package/templates/src/utils/api/brandHelpers.ts +24 -1
  66. package/templates/src/utils/api/emailHelpers.ts +105 -0
  67. package/templates/src/utils/booking/appointmentMode.ts +135 -0
  68. package/templates/src/utils/customHelpers.ts +2 -0
  69. package/templates/src/utils/tenantResolver.ts +1 -1
  70. package/utils/inject-files.ts +63 -5
  71. package/templates/src/components/codehooks/SandboxAuthWrapper.tsx +0 -101
  72. package/templates/src/utils/actions/actionButton.ts +0 -103
  73. package/templates/src/utils/actions/preParse_Clicked.ts +0 -87
@@ -1,5 +1,10 @@
1
1
  import { useStore } from '@nanostores/react';
2
- import { cartStore, addQueue, type CartAction } from '@/stores/shopify';
2
+ import {
3
+ cartStore,
4
+ addQueue,
5
+ getCartItemKey,
6
+ type CartAction,
7
+ } from '@/stores/shopify';
3
8
  import type { ResourceNode } from '@/types/compositorTypes';
4
9
 
5
10
  interface Props {
@@ -11,6 +16,8 @@ interface Props {
11
16
  };
12
17
  }
13
18
 
19
+ const HEX_BG_RE = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
20
+
14
21
  export default function ShopifyServiceList({ resources = {}, options }: Props) {
15
22
  const cart = useStore(cartStore);
16
23
 
@@ -18,9 +25,18 @@ export default function ShopifyServiceList({ resources = {}, options }: Props) {
18
25
  let services = resources['service'] || [];
19
26
 
20
27
  let group = '';
28
+ let title = '';
29
+ let bgColor = '#f9f9f9';
21
30
  try {
22
31
  const parsedOptions = JSON.parse(options?.params?.options || '{}');
23
- group = parsedOptions.group || '';
32
+ group = typeof parsedOptions.group === 'string' ? parsedOptions.group : '';
33
+ if (typeof parsedOptions.title === 'string') {
34
+ title = parsedOptions.title.trim();
35
+ }
36
+ const rawBg = parsedOptions.bgColor;
37
+ if (typeof rawBg === 'string' && HEX_BG_RE.test(rawBg)) {
38
+ bgColor = rawBg;
39
+ }
24
40
  } catch (e) {
25
41
  // Ignore JSON parse errors
26
42
  }
@@ -81,55 +97,83 @@ export default function ShopifyServiceList({ resources = {}, options }: Props) {
81
97
  }
82
98
 
83
99
  return (
84
- <div className="space-y-4">
85
- {displayServices.map((resource) => {
86
- const variantId = getServiceVariantId(resource);
87
- const key = variantId || `${resource.id}_null_null`;
88
-
89
- const cartItem = cart[key];
90
- const isSelected = (cartItem?.quantity || 0) > 0;
91
- const duration = resource.optionsPayload?.bookingLengthMinutes;
92
-
93
- return (
94
- <div
95
- key={resource.id}
96
- className={`flex items-center justify-between rounded-lg border p-4 transition-colors ${
97
- isSelected
98
- ? 'border-black bg-gray-50'
99
- : 'border-gray-200 bg-white hover:border-gray-300'
100
- }`}
101
- >
102
- <div className="flex-grow">
103
- <div className="flex items-center gap-2">
104
- <h3 className="font-bold text-gray-900">{resource.title}</h3>
105
- {duration && (
106
- <span className="inline-flex items-center rounded-sm bg-blue-50 px-2 py-0.5 text-xs font-bold text-blue-700">
107
- {duration} mins
108
- </span>
109
- )}
110
- </div>
111
- <p className="mt-1 text-sm text-gray-500">{resource.oneliner}</p>
112
- </div>
113
-
114
- <div className="ml-4 flex-shrink-0">
115
- <button
116
- onClick={() => handleToggle(resource, cartItem?.quantity || 0)}
117
- className={`relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none ${
118
- isSelected ? 'bg-black' : 'bg-gray-200'
119
- }`}
120
- role="switch"
121
- aria-checked={isSelected}
122
- >
123
- <span
124
- className={`pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out ${
125
- isSelected ? 'translate-x-5' : 'translate-x-0'
100
+ <section className="w-full">
101
+ <div
102
+ className="flex w-full flex-col p-12 md:p-12 xl:p-16"
103
+ style={{ backgroundColor: bgColor }}
104
+ >
105
+ {title ? (
106
+ <header className="max-w-4xl">
107
+ <h3
108
+ className="mb-6 text-balance font-action text-2xl font-bold md:text-3xl xl:text-4xl"
109
+ style={{ color: '#2d2923' }}
110
+ >
111
+ {title}
112
+ </h3>
113
+ </header>
114
+ ) : null}
115
+ <section className="w-full">
116
+ <div className="space-y-4">
117
+ {displayServices.map((resource) => {
118
+ const variantId = getServiceVariantId(resource);
119
+ const key = getCartItemKey({
120
+ resourceId: resource.id,
121
+ variantId,
122
+ });
123
+
124
+ const cartItem = cart[key];
125
+ const isSelected = (cartItem?.quantity || 0) > 0;
126
+ const duration = resource.optionsPayload?.bookingLengthMinutes;
127
+
128
+ return (
129
+ <div
130
+ key={resource.id}
131
+ className={`flex items-center justify-between rounded-lg border p-4 transition-colors ${
132
+ isSelected
133
+ ? 'border-black bg-gray-50'
134
+ : 'border-gray-200 bg-white hover:border-gray-300'
126
135
  }`}
127
- />
128
- </button>
129
- </div>
136
+ >
137
+ <div className="flex-grow">
138
+ <div className="flex items-center gap-2">
139
+ <h3 className="font-bold text-gray-900">
140
+ {resource.title}
141
+ </h3>
142
+ {duration && (
143
+ <span className="inline-flex items-center rounded-sm bg-blue-50 px-2 py-0.5 text-xs font-bold text-blue-700">
144
+ {duration} mins
145
+ </span>
146
+ )}
147
+ </div>
148
+ <p className="mt-1 text-sm text-gray-500">
149
+ {resource.oneliner}
150
+ </p>
151
+ </div>
152
+
153
+ <div className="ml-4 flex-shrink-0">
154
+ <button
155
+ onClick={() =>
156
+ handleToggle(resource, cartItem?.quantity || 0)
157
+ }
158
+ className={`relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none ${
159
+ isSelected ? 'bg-black' : 'bg-gray-200'
160
+ }`}
161
+ role="switch"
162
+ aria-checked={isSelected}
163
+ >
164
+ <span
165
+ className={`pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out ${
166
+ isSelected ? 'translate-x-5' : 'translate-x-0'
167
+ }`}
168
+ />
169
+ </button>
170
+ </div>
171
+ </div>
172
+ );
173
+ })}
130
174
  </div>
131
- );
132
- })}
133
- </div>
175
+ </section>
176
+ </div>
177
+ </section>
134
178
  );
135
179
  }
@@ -18,6 +18,12 @@ const resources = await getHeaderResources(tenantId, resourceCategories);
18
18
 
19
19
  <Layout title="Your Cart" slug="cart">
20
20
  <main class="mx-auto max-w-7xl px-4 py-16 md:px-6 xl:px-8">
21
- <Cart resources={resources} embedded={true} client:only="react" />
21
+ <Cart
22
+ resources={resources}
23
+ allowRemote={brandConfig?.SCHEDULING?.allowRemote || false}
24
+ remoteOnly={brandConfig?.SCHEDULING?.remoteOnly || false}
25
+ embedded={true}
26
+ client:only="react"
27
+ />
22
28
  </main>
23
29
  </Layout>
@@ -58,7 +58,7 @@ if (menu?.optionsPayload) {
58
58
  return item;
59
59
  });
60
60
 
61
- allLinks = additionalLinks.concat(featuredLinks);
61
+ allLinks = featuredLinks.concat(additionalLinks);
62
62
  }
63
63
 
64
64
  // Parse socials string
@@ -106,7 +106,7 @@ const createdDate = created ? new Date(created) : new Date();
106
106
  {allLinks.map((item: any) => (
107
107
  <a
108
108
  href={item.to}
109
- class="z-10 whitespace-nowrap rounded bg-brand-7 px-3.5 py-1.5 text-lg text-white shadow-sm transition-colors hover:bg-myblack hover:text-white focus:bg-brand-7 focus:text-white"
109
+ class="z-10 whitespace-nowrap rounded-md bg-brand-7 px-3.5 py-1.5 text-lg text-white shadow-sm transition-colors hover:bg-myblack hover:text-white focus:bg-brand-7 focus:text-white"
110
110
  title={item.description}
111
111
  >
112
112
  <span class="font-bold">{item.name}</span>
@@ -82,7 +82,9 @@ if (hasShopify) {
82
82
  <CheckoutModal
83
83
  client:only="react"
84
84
  resources={shopifyResources}
85
- maxLength={brandConfig?.scheduling?.maxLengthMinutes || 180}
85
+ maxLength={brandConfig?.SCHEDULING?.maxLengthMinutes || 180}
86
+ allowRemote={brandConfig?.SCHEDULING?.allowRemote || false}
87
+ remoteOnly={brandConfig?.SCHEDULING?.remoteOnly || false}
86
88
  />
87
89
  )}
88
90
  </>
@@ -123,13 +125,15 @@ if (hasShopify) {
123
125
 
124
126
  {
125
127
  !!menu ? (
126
- <Menu
127
- payload={menu}
128
- slug={slug}
129
- brandConfig={brandConfig}
130
- isContext={isContext}
131
- client:load
132
- />
128
+ <div class="flex min-w-0 flex-1 justify-end">
129
+ <Menu
130
+ payload={menu}
131
+ slug={slug}
132
+ brandConfig={brandConfig}
133
+ isContext={isContext}
134
+ client:load
135
+ />
136
+ </div>
133
137
  ) : null
134
138
  }
135
139
  </div>
@@ -368,7 +372,11 @@ if (hasShopify) {
368
372
  {
369
373
  !isStoryKeep && hasShopify && (
370
374
  <>
371
- <ShopifyCartManager resources={shopifyResources} client:only="react" />
375
+ <ShopifyCartManager
376
+ resources={shopifyResources}
377
+ brandConfig={brandConfig}
378
+ client:only="react"
379
+ />
372
380
  <CartModal client:only="react" />
373
381
  </>
374
382
  )
@@ -1,38 +1,12 @@
1
- import { Menu } from '@ark-ui/react';
1
+ import { useState } from 'react';
2
+ import { Dialog } from '@ark-ui/react/dialog';
2
3
  import { Portal } from '@ark-ui/react/portal';
3
4
  import ChevronDownIcon from '@heroicons/react/20/solid/ChevronDownIcon';
5
+ import XMarkIcon from '@heroicons/react/24/outline/XMarkIcon';
4
6
  import { lispLexer } from '@/utils/actions/lispLexer';
5
7
  import { preParseAction } from '@/utils/actions/preParse_Action';
6
8
  import type { LispToken } from '@/types/compositorTypes';
7
9
 
8
- // CSS to style the menu items with hover and selection states
9
- const menuStyles = `
10
- .menu-content {
11
- transition-property: opacity, transform;
12
- transition-duration: 200ms;
13
- z-index: 10050;
14
- }
15
-
16
- .menu-content[data-state="open"] {
17
- opacity: 1;
18
- transform: translateY(0);
19
- }
20
-
21
- .menu-content[data-state="closed"] {
22
- opacity: 0;
23
- transform: translateY(1px);
24
- }
25
-
26
- .menu-item[data-highlighted] {
27
- background-color: #f3f4f6;
28
- }
29
-
30
- .menu-item:focus {
31
- outline: 2px solid #0891b2;
32
- outline-offset: -2px;
33
- }
34
- `;
35
-
36
10
  interface MenuLink {
37
11
  name: string;
38
12
  description: string;
@@ -63,6 +37,7 @@ interface MenuProps {
63
37
  const MenuComponent = (props: MenuProps) => {
64
38
  const { payload, slug, isContext, brandConfig } = props;
65
39
  const thisPayload = payload.optionsPayload;
40
+ const [mobileOpen, setMobileOpen] = useState(false);
66
41
 
67
42
  function processMenuLink(e: MenuLink): ProcessedMenuLinkDatum {
68
43
  const item = { ...e } as ProcessedMenuLinkDatum;
@@ -181,12 +156,112 @@ const MenuComponent = (props: MenuProps) => {
181
156
  );
182
157
  };
183
158
 
159
+ const MobileMenuItem = ({ item }: { item: ProcessedMenuLinkDatum }) => {
160
+ if (item.renderAs === 'button') {
161
+ return (
162
+ <button
163
+ type="button"
164
+ className="block w-full rounded-xl p-4 text-left hover:bg-mygreen/20 focus:outline-none focus:ring-2 focus:ring-myblue"
165
+ aria-label={`${item.name} - ${item.description}`}
166
+ hx-post="/api/v1/state"
167
+ hx-swap="none"
168
+ hx-vals={item.htmxVals}
169
+ onClick={() => setMobileOpen(false)}
170
+ >
171
+ <p className="text-xl font-bold leading-7 text-myblack">
172
+ {item.name}
173
+ </p>
174
+ <p className="mt-1 text-base leading-6 text-mydarkgrey">
175
+ {item.description}
176
+ </p>
177
+ </button>
178
+ );
179
+ }
180
+
181
+ if (item.renderAs === 'a') {
182
+ return (
183
+ <a
184
+ href={item.href}
185
+ className="block w-full rounded-xl p-4 text-left hover:bg-mygreen/20 focus:outline-none focus:ring-2 focus:ring-myblue"
186
+ aria-label={`${item.name} - ${item.description}`}
187
+ onClick={() => setMobileOpen(false)}
188
+ >
189
+ <p className="text-xl font-bold leading-7 text-myblack">
190
+ {item.name}
191
+ </p>
192
+ <p className="mt-1 text-base leading-6 text-mydarkgrey">
193
+ {item.description}
194
+ </p>
195
+ </a>
196
+ );
197
+ }
198
+
199
+ return (
200
+ <span
201
+ className="block w-full rounded-xl p-4 text-left opacity-60"
202
+ aria-label={`${item.name} - ${item.description}`}
203
+ >
204
+ <p className="text-xl font-bold leading-7 text-myblack">{item.name}</p>
205
+ <p className="mt-1 text-base leading-6 text-mydarkgrey">
206
+ {item.description}
207
+ </p>
208
+ </span>
209
+ );
210
+ };
211
+
212
+ const MobileCompactItem = ({ item }: { item: ProcessedMenuLinkDatum }) => {
213
+ if (item.renderAs === 'button') {
214
+ return (
215
+ <button
216
+ type="button"
217
+ className="block w-full rounded-lg p-3 text-left hover:bg-mygreen/20 focus:outline-none focus:ring-2 focus:ring-myblue"
218
+ title={item.description}
219
+ aria-label={`${item.name} - ${item.description}`}
220
+ hx-post="/api/v1/state"
221
+ hx-swap="none"
222
+ hx-vals={item.htmxVals}
223
+ onClick={() => setMobileOpen(false)}
224
+ >
225
+ <span className="block text-base font-bold leading-6 text-mydarkgrey">
226
+ {item.name}
227
+ </span>
228
+ </button>
229
+ );
230
+ }
231
+
232
+ if (item.renderAs === 'a') {
233
+ return (
234
+ <a
235
+ href={item.href}
236
+ className="block w-full rounded-lg p-3 text-left hover:bg-mygreen/20 focus:outline-none focus:ring-2 focus:ring-myblue"
237
+ title={item.description}
238
+ aria-label={`${item.name} - ${item.description}`}
239
+ onClick={() => setMobileOpen(false)}
240
+ >
241
+ <span className="block text-base font-bold leading-6 text-mydarkgrey">
242
+ {item.name}
243
+ </span>
244
+ </a>
245
+ );
246
+ }
247
+
248
+ return (
249
+ <span
250
+ className="block w-full rounded-lg p-3 text-left opacity-60"
251
+ title={item.description}
252
+ aria-label={`${item.name} - ${item.description}`}
253
+ >
254
+ <span className="block text-base font-bold leading-6 text-mydarkgrey">
255
+ {item.name}
256
+ </span>
257
+ </span>
258
+ );
259
+ };
260
+
184
261
  return (
185
262
  <>
186
- <style dangerouslySetInnerHTML={{ __html: menuStyles }} />
187
-
188
263
  {/* Desktop Navigation */}
189
- <nav className="ml-6 hidden flex-wrap items-center justify-end space-x-3 font-action md:flex md:space-x-6">
264
+ <nav className="ml-6 hidden min-w-0 max-w-full flex-wrap items-center justify-end space-x-3 font-action md:flex md:space-x-6">
190
265
  {featuredLinks.map((item: ProcessedMenuLinkDatum) => (
191
266
  <div key={item.name} className="relative py-1.5">
192
267
  <InteractiveMenuItem item={item} />
@@ -196,134 +271,81 @@ const MenuComponent = (props: MenuProps) => {
196
271
 
197
272
  {/* Mobile Navigation Menu */}
198
273
  <div className="font-action md:hidden">
199
- <Menu.Root>
200
- <Menu.Trigger
274
+ <Dialog.Root
275
+ open={mobileOpen}
276
+ onOpenChange={(details) => setMobileOpen(details.open)}
277
+ >
278
+ <Dialog.Trigger
201
279
  className="inline-flex rounded-md px-3 py-2 text-xl font-bold text-myblue hover:text-black focus:outline-none focus:ring-2 focus:ring-myblue"
202
280
  aria-label="Open navigation menu"
203
281
  >
204
282
  <span>MENU</span>
205
283
  <ChevronDownIcon className="ml-1 h-5 w-5" aria-hidden="true" />
206
- </Menu.Trigger>
284
+ </Dialog.Trigger>
207
285
 
208
286
  <Portal>
209
- <Menu.Positioner>
210
- <Menu.Content className="menu-content mt-5 flex">
211
- <div className="w-screen">
212
- <div className="text-md flex-auto overflow-hidden rounded-3xl bg-white p-4 leading-6 shadow-lg ring-1 ring-mydarkgrey/5">
213
- {/* Featured Links Section */}
214
- <div className="px-8">
287
+ <Dialog.Backdrop
288
+ className="fixed inset-0 bg-black/40"
289
+ style={{ zIndex: 10050 }}
290
+ />
291
+ <Dialog.Positioner
292
+ className="fixed inset-0"
293
+ style={{ zIndex: 10051 }}
294
+ >
295
+ <Dialog.Content className="h-full w-full overflow-hidden bg-white">
296
+ <div className="flex h-full flex-col overflow-hidden">
297
+ <div className="border-b border-mylightgrey px-4 py-3">
298
+ <div className="flex items-center justify-between gap-3">
299
+ <Dialog.Title className="text-lg font-bold text-myblack">
300
+ Navigation Menu
301
+ </Dialog.Title>
302
+ <Dialog.CloseTrigger
303
+ className="rounded-full p-2 text-mydarkgrey hover:bg-mylightgrey focus:outline-none focus:ring-2 focus:ring-myblue"
304
+ aria-label="Close navigation menu"
305
+ >
306
+ <XMarkIcon className="h-6 w-6" aria-hidden="true" />
307
+ </Dialog.CloseTrigger>
308
+ </div>
309
+ </div>
310
+
311
+ <div className="min-h-0 flex-1 overflow-y-auto overflow-x-hidden px-4 py-4">
312
+ <ul role="list" className="space-y-3">
215
313
  {featuredLinks.map((item: ProcessedMenuLinkDatum) => (
216
- <Menu.Item
217
- key={item.name}
218
- value={item.name}
219
- className="menu-item group relative flex gap-x-6 rounded-lg p-4 hover:bg-mygreen/20"
220
- >
221
- <div>
222
- {item.renderAs === 'button' ? (
223
- <button
224
- type="button"
225
- className="font-action text-xl text-myblack hover:text-black focus:text-black focus:outline-none"
226
- aria-label={`${item.name} - ${item.description}`}
227
- hx-post="/api/v1/state"
228
- hx-swap="none"
229
- hx-vals={item.htmxVals}
230
- >
231
- {item.name}
232
- <span className="absolute inset-0" />
233
- </button>
234
- ) : item.renderAs === 'a' ? (
235
- <a
236
- href={item.href}
237
- className="font-action text-xl text-myblack hover:text-black focus:text-black focus:outline-none"
238
- aria-label={`${item.name} - ${item.description}`}
239
- >
240
- {item.name}
241
- <span className="absolute inset-0" />
242
- </a>
243
- ) : (
244
- <span
245
- className="font-action text-xl text-myblack opacity-50"
246
- aria-label={`${item.name} - ${item.description}`}
247
- >
248
- {item.name}
249
- </span>
250
- )}
251
- <p className="mt-1 text-mydarkgrey">
252
- {item.description}
253
- </p>
254
- </div>
255
- </Menu.Item>
314
+ <li key={item.name} className="min-w-0">
315
+ <MobileMenuItem item={item} />
316
+ </li>
256
317
  ))}
257
- </div>
318
+ </ul>
258
319
 
259
- {/* Additional Links Section */}
260
320
  {additionalLinks.length > 0 && (
261
- <div className="bg-slate-50 p-8">
262
- <div className="flex justify-between">
263
- <h3
264
- className="mt-4 text-sm leading-6 text-myblue"
265
- id="additional-links-heading"
266
- >
267
- Additional Links
268
- </h3>
269
- </div>
321
+ <section className="mt-6 rounded-xl bg-slate-50 p-4">
322
+ <h3
323
+ className="text-sm font-bold leading-6 text-myblue"
324
+ id="additional-links-heading"
325
+ >
326
+ Additional Links
327
+ </h3>
270
328
  <ul
271
329
  role="list"
272
- className="mt-6 space-y-6"
330
+ className="mt-3 space-y-2"
273
331
  aria-labelledby="additional-links-heading"
274
332
  >
275
333
  {additionalLinks.map(
276
334
  (item: ProcessedMenuLinkDatum) => (
277
- <li key={item.name} className="relative">
278
- <Menu.Item
279
- value={item.name}
280
- className="menu-item block w-full text-left"
281
- >
282
- {item.renderAs === 'button' ? (
283
- <button
284
- type="button"
285
- className="block truncate rounded p-2 text-sm font-bold leading-6 text-mydarkgrey hover:text-black focus:text-black focus:underline focus:outline-none"
286
- title={item.description}
287
- aria-label={`${item.name} - ${item.description}`}
288
- hx-post="/api/v1/state"
289
- hx-swap="none"
290
- hx-vals={item.htmxVals}
291
- >
292
- {item.name}
293
- <span className="absolute inset-0" />
294
- </button>
295
- ) : item.renderAs === 'a' ? (
296
- <a
297
- href={item.href}
298
- className="block truncate rounded p-2 text-sm font-bold leading-6 text-mydarkgrey hover:text-black focus:text-black focus:underline focus:outline-none"
299
- title={item.description}
300
- aria-label={`${item.name} - ${item.description}`}
301
- >
302
- {item.name}
303
- <span className="absolute inset-0" />
304
- </a>
305
- ) : (
306
- <span
307
- className="block truncate rounded p-2 text-sm font-bold leading-6 text-mydarkgrey opacity-50"
308
- title={item.description}
309
- aria-label={`${item.name} - ${item.description}`}
310
- >
311
- {item.name}
312
- </span>
313
- )}
314
- </Menu.Item>
335
+ <li key={item.name} className="min-w-0">
336
+ <MobileCompactItem item={item} />
315
337
  </li>
316
338
  )
317
339
  )}
318
340
  </ul>
319
- </div>
341
+ </section>
320
342
  )}
321
343
  </div>
322
344
  </div>
323
- </Menu.Content>
324
- </Menu.Positioner>
345
+ </Dialog.Content>
346
+ </Dialog.Positioner>
325
347
  </Portal>
326
- </Menu.Root>
348
+ </Dialog.Root>
327
349
  </div>
328
350
  </>
329
351
  );
@@ -1,4 +1,4 @@
1
- import { useState, useEffect, useRef } from 'react';
1
+ import { useState, useEffect, useRef, type FocusEvent } from 'react';
2
2
  import XMarkIcon from '@heroicons/react/24/outline/XMarkIcon';
3
3
  import PlusIcon from '@heroicons/react/24/outline/PlusIcon';
4
4
  import TrashIcon from '@heroicons/react/24/outline/TrashIcon';
@@ -162,7 +162,7 @@ const BunnyVideoSetup = ({ nodeId, params }: BunnyVideoSetupProps) => {
162
162
  };
163
163
 
164
164
  const saveChanges = (
165
- customChapters?: Chapter[] | React.FocusEvent<HTMLInputElement>
165
+ customChapters?: Chapter[] | FocusEvent<HTMLInputElement>
166
166
  ) => {
167
167
  if (!customChapters || 'target' in customChapters) {
168
168
  updatePaneNode();