@vendure/dashboard 3.5.0-minor-202510071456 → 3.5.0-minor-202510201346

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 (204) hide show
  1. package/dist/plugin/dashboard.plugin.js +1 -1
  2. package/dist/vite/utils/ast-utils.spec.js +3 -3
  3. package/dist/vite/vite-plugin-hmr.d.ts +8 -0
  4. package/dist/vite/vite-plugin-hmr.js +34 -0
  5. package/dist/vite/vite-plugin-theme.js +6 -6
  6. package/dist/vite/vite-plugin-transform-index.js +6 -1
  7. package/dist/vite/vite-plugin-vendure-dashboard.d.ts +31 -4
  8. package/dist/vite/vite-plugin-vendure-dashboard.js +89 -34
  9. package/package.json +17 -5
  10. package/src/app/app-providers.tsx +4 -1
  11. package/src/app/common/map-faceted-filter-fields.ts +21 -0
  12. package/src/app/main.tsx +3 -1
  13. package/src/app/routes/_authenticated/_administrators/administrators.graphql.ts +2 -2
  14. package/src/app/routes/_authenticated/_administrators/administrators.tsx +13 -3
  15. package/src/app/routes/_authenticated/_administrators/administrators_.$id.tsx +6 -13
  16. package/src/app/routes/_authenticated/_administrators/components/role-permissions-display.tsx +1 -1
  17. package/src/app/routes/_authenticated/_assets/assets.tsx +17 -1
  18. package/src/app/routes/_authenticated/_collections/collections.graphql.ts +1 -0
  19. package/src/app/routes/_authenticated/_collections/collections.tsx +5 -0
  20. package/src/app/routes/_authenticated/_collections/components/collection-bulk-actions.tsx +0 -1
  21. package/src/app/routes/_authenticated/_customers/customers.tsx +9 -5
  22. package/src/app/routes/_authenticated/_facets/components/facet-bulk-actions.tsx +0 -6
  23. package/src/app/routes/_authenticated/_facets/components/facet-value-bulk-actions.tsx +16 -0
  24. package/src/app/routes/_authenticated/_facets/components/facet-values-table.tsx +43 -12
  25. package/src/app/routes/_authenticated/_facets/facets_.$facetId.values_.$id.tsx +14 -5
  26. package/src/app/routes/_authenticated/_orders/components/edit-order-table.tsx +117 -92
  27. package/src/app/routes/_authenticated/_orders/components/order-detail-shared.tsx +1 -1
  28. package/src/app/routes/_authenticated/_orders/components/order-modification-summary.tsx +2 -1
  29. package/src/app/routes/_authenticated/_orders/components/order-table-totals.tsx +26 -27
  30. package/src/app/routes/_authenticated/_orders/components/order-table.tsx +5 -3
  31. package/src/app/routes/_authenticated/_orders/components/state-transition-control.tsx +6 -9
  32. package/src/app/routes/_authenticated/_orders/orders.graphql.ts +17 -1
  33. package/src/app/routes/_authenticated/_orders/orders.tsx +2 -0
  34. package/src/app/routes/_authenticated/_orders/orders_.$id_.modify.tsx +48 -281
  35. package/src/app/routes/_authenticated/_orders/orders_.draft.$id.tsx +59 -40
  36. package/src/app/routes/_authenticated/_orders/utils/order-utils.ts +73 -0
  37. package/src/app/routes/_authenticated/_orders/utils/use-modify-order.ts +312 -0
  38. package/src/app/routes/_authenticated/_payment-methods/payment-methods.graphql.ts +2 -2
  39. package/src/app/routes/_authenticated/_payment-methods/payment-methods.tsx +4 -0
  40. package/src/app/routes/_authenticated/_product-variants/product-variants.tsx +2 -0
  41. package/src/app/routes/_authenticated/_products/components/product-bulk-actions.tsx +0 -6
  42. package/src/app/routes/_authenticated/_products/products.tsx +6 -2
  43. package/src/app/routes/_authenticated/_products/products_.$productId.option-groups.$productOptionGroupId.options_.$id.tsx +4 -8
  44. package/src/app/routes/_authenticated/_promotions/components/promotion-bulk-actions.tsx +0 -10
  45. package/src/app/routes/_authenticated/_promotions/promotions.graphql.ts +2 -2
  46. package/src/app/routes/_authenticated/_promotions/promotions.tsx +12 -0
  47. package/src/app/routes/_authenticated/_promotions/promotions_.$id.tsx +6 -2
  48. package/src/app/routes/_authenticated/_sellers/sellers.graphql.ts +2 -2
  49. package/src/app/routes/_authenticated/_shipping-methods/shipping-methods.graphql.ts +2 -2
  50. package/src/app/routes/_authenticated/_shipping-methods/shipping-methods.tsx +4 -0
  51. package/src/app/routes/_authenticated/_shipping-methods/shipping-methods_.$id.tsx +4 -10
  52. package/src/app/routes/_authenticated/_stock-locations/stock-locations.graphql.ts +2 -2
  53. package/src/app/routes/_authenticated/_tax-categories/tax-categories.graphql.ts +2 -2
  54. package/src/app/routes/_authenticated/_tax-rates/tax-rates.tsx +9 -0
  55. package/src/app/routes/_authenticated/_tax-rates/tax-rates_.$id.tsx +1 -0
  56. package/src/app/routes/_authenticated/_zones/zones.graphql.ts +2 -2
  57. package/src/app/routes/login.tsx +2 -2
  58. package/src/i18n/locales/ar.po +420 -289
  59. package/src/i18n/locales/cs.po +420 -289
  60. package/src/i18n/locales/de.po +420 -289
  61. package/src/i18n/locales/en.po +420 -289
  62. package/src/i18n/locales/es.po +420 -289
  63. package/src/i18n/locales/fa.po +420 -289
  64. package/src/i18n/locales/fr.po +468 -337
  65. package/src/i18n/locales/he.po +420 -289
  66. package/src/i18n/locales/hr.po +420 -289
  67. package/src/i18n/locales/it.po +420 -289
  68. package/src/i18n/locales/ja.po +420 -289
  69. package/src/i18n/locales/nb.po +420 -289
  70. package/src/i18n/locales/ne.po +420 -289
  71. package/src/i18n/locales/pl.po +420 -289
  72. package/src/i18n/locales/pt_BR.po +420 -289
  73. package/src/i18n/locales/pt_PT.po +420 -289
  74. package/src/i18n/locales/ru.po +420 -289
  75. package/src/i18n/locales/sv.po +420 -289
  76. package/src/i18n/locales/tr.po +420 -289
  77. package/src/i18n/locales/uk.po +420 -289
  78. package/src/i18n/locales/zh_Hans.po +420 -289
  79. package/src/i18n/locales/zh_Hant.po +420 -289
  80. package/src/lib/components/data-input/affixed-input.stories.tsx +93 -0
  81. package/src/lib/components/data-input/affixed-input.tsx +5 -2
  82. package/src/lib/components/data-input/boolean-input.stories.tsx +102 -0
  83. package/src/lib/components/data-input/checkbox-input.stories.tsx +61 -0
  84. package/src/lib/components/data-input/datetime-input.stories.tsx +62 -0
  85. package/src/lib/components/data-input/datetime-input.tsx +27 -13
  86. package/src/lib/components/data-input/default-relation-input.tsx +18 -12
  87. package/src/lib/components/data-input/money-input.stories.tsx +88 -0
  88. package/src/lib/components/data-input/number-input.stories.tsx +103 -0
  89. package/src/lib/components/data-input/number-input.tsx +10 -4
  90. package/src/lib/components/data-input/password-form-input.stories.tsx +65 -0
  91. package/src/lib/components/data-input/{password-input.tsx → password-form-input.tsx} +1 -1
  92. package/src/lib/components/data-input/rich-text-input.stories.tsx +92 -0
  93. package/src/lib/components/data-input/slug-input.stories.tsx +232 -0
  94. package/src/lib/components/data-input/slug-input.tsx +9 -10
  95. package/src/lib/components/data-input/text-input.stories.tsx +52 -0
  96. package/src/lib/components/data-input/textarea-input.stories.tsx +55 -0
  97. package/src/lib/components/data-table/add-filter-menu.tsx +6 -1
  98. package/src/lib/components/data-table/column-header-wrapper.tsx +106 -0
  99. package/src/lib/components/data-table/data-table-bulk-action-item.tsx +11 -9
  100. package/src/lib/components/data-table/data-table-bulk-actions.tsx +4 -4
  101. package/src/lib/components/data-table/data-table-column-header.tsx +17 -14
  102. package/src/lib/components/data-table/data-table-faceted-filter.tsx +33 -11
  103. package/src/lib/components/data-table/data-table-filter-badge-editable.tsx +35 -0
  104. package/src/lib/components/data-table/data-table-filter-badge.tsx +23 -16
  105. package/src/lib/components/data-table/data-table-filter-dialog.tsx +28 -8
  106. package/src/lib/components/data-table/data-table-pagination.tsx +23 -7
  107. package/src/lib/components/data-table/data-table.stories.tsx +249 -0
  108. package/src/lib/components/data-table/data-table.tsx +37 -9
  109. package/src/lib/components/data-table/filters/data-table-datetime-filter.tsx +79 -34
  110. package/src/lib/components/data-table/use-generated-columns.tsx +55 -27
  111. package/src/lib/components/layout/nav-user.tsx +19 -13
  112. package/src/lib/components/login/login-form.tsx +39 -123
  113. package/src/lib/components/shared/alerts.tsx +29 -17
  114. package/src/lib/components/shared/asset/asset-bulk-actions.tsx +3 -3
  115. package/src/lib/components/shared/asset/asset-gallery.stories.tsx +76 -0
  116. package/src/lib/components/shared/asset/asset-gallery.tsx +147 -113
  117. package/src/lib/components/shared/asset/asset-picker-dialog.stories.tsx +58 -0
  118. package/src/lib/components/shared/customer-group-selector.tsx +5 -2
  119. package/src/lib/components/shared/detail-page-button.stories.tsx +52 -0
  120. package/src/lib/components/shared/facet-value-selector.stories.tsx +48 -0
  121. package/src/lib/components/shared/facet-value-selector.tsx +130 -34
  122. package/src/lib/components/shared/paginated-list-data-table.stories.tsx +212 -0
  123. package/src/lib/components/shared/paginated-list-data-table.tsx +12 -12
  124. package/src/lib/components/shared/permission-guard.stories.tsx +46 -0
  125. package/src/lib/components/shared/remove-from-channel-bulk-action.tsx +2 -0
  126. package/src/lib/components/shared/rich-text-editor/responsive-toolbar.tsx +8 -4
  127. package/src/lib/components/shared/rich-text-editor/rich-text-editor.tsx +1 -0
  128. package/src/lib/components/shared/table-cell/order-table-cell-components.tsx +40 -0
  129. package/src/lib/components/shared/vendure-image.stories.tsx +167 -0
  130. package/src/lib/components/shared/vendure-image.tsx +6 -7
  131. package/src/lib/components/ui/accordion.stories.tsx +33 -0
  132. package/src/lib/components/ui/alert-dialog.stories.tsx +48 -0
  133. package/src/lib/components/ui/alert.stories.tsx +35 -0
  134. package/src/lib/components/ui/aspect-ratio.stories.tsx +28 -0
  135. package/src/lib/components/ui/badge.stories.tsx +28 -0
  136. package/src/lib/components/ui/breadcrumb.stories.tsx +41 -0
  137. package/src/lib/components/ui/button.stories.tsx +38 -0
  138. package/src/lib/components/ui/calendar.stories.tsx +22 -0
  139. package/src/lib/components/ui/card.stories.tsx +28 -0
  140. package/src/lib/components/ui/carousel.stories.tsx +34 -0
  141. package/src/lib/components/ui/checkbox.stories.tsx +31 -0
  142. package/src/lib/components/ui/collapsible.stories.tsx +39 -0
  143. package/src/lib/components/ui/command.stories.tsx +44 -0
  144. package/src/lib/components/ui/context-menu.stories.tsx +38 -0
  145. package/src/lib/components/ui/dialog.stories.tsx +52 -0
  146. package/src/lib/components/ui/drawer.stories.tsx +50 -0
  147. package/src/lib/components/ui/dropdown-menu.stories.tsx +41 -0
  148. package/src/lib/components/ui/hover-card.stories.tsx +38 -0
  149. package/src/lib/components/ui/input-group.tsx +148 -0
  150. package/src/lib/components/ui/input-otp.stories.tsx +30 -0
  151. package/src/lib/components/ui/input.stories.tsx +38 -0
  152. package/src/lib/components/ui/label.stories.tsx +24 -0
  153. package/src/lib/components/ui/menubar.stories.tsx +53 -0
  154. package/src/lib/components/ui/navigation-menu.stories.tsx +54 -0
  155. package/src/lib/components/ui/pagination.stories.tsx +51 -0
  156. package/src/lib/components/ui/password-input.stories.tsx +32 -0
  157. package/src/lib/components/ui/password-input.tsx +29 -0
  158. package/src/lib/components/ui/popover.stories.tsx +33 -0
  159. package/src/lib/components/ui/progress.stories.tsx +27 -0
  160. package/src/lib/components/ui/radio-group.stories.tsx +34 -0
  161. package/src/lib/components/ui/resizable.stories.tsx +32 -0
  162. package/src/lib/components/ui/scroll-area.stories.tsx +31 -0
  163. package/src/lib/components/ui/select.stories.tsx +36 -0
  164. package/src/lib/components/ui/separator.stories.tsx +35 -0
  165. package/src/lib/components/ui/sheet.stories.tsx +50 -0
  166. package/src/lib/components/ui/sidebar-context.ts +16 -0
  167. package/src/lib/components/ui/sidebar.tsx +2 -13
  168. package/src/lib/components/ui/skeleton.stories.tsx +26 -0
  169. package/src/lib/components/ui/slider.stories.tsx +37 -0
  170. package/src/lib/components/ui/switch.stories.tsx +31 -0
  171. package/src/lib/components/ui/table.stories.tsx +52 -0
  172. package/src/lib/components/ui/tabs.stories.tsx +29 -0
  173. package/src/lib/components/ui/textarea.stories.tsx +32 -0
  174. package/src/lib/components/ui/toggle-group.stories.tsx +31 -0
  175. package/src/lib/components/ui/toggle.stories.tsx +39 -0
  176. package/src/lib/components/ui/tooltip.stories.tsx +30 -0
  177. package/src/lib/components/ui/tooltip.tsx +2 -2
  178. package/src/lib/framework/alert/alert-extensions.tsx +0 -11
  179. package/src/lib/framework/alert/alert-item.tsx +14 -19
  180. package/src/lib/framework/alert/alerts-indicator.tsx +14 -15
  181. package/src/lib/framework/alert/search-index-buffer-alert/search-index-buffer-alert.ts +41 -0
  182. package/src/lib/framework/component-registry/component-registry.tsx +3 -14
  183. package/src/lib/framework/dashboard-widget/base-widget.tsx +18 -9
  184. package/src/lib/framework/dashboard-widget/widget-filters-context.tsx +12 -11
  185. package/src/lib/framework/defaults.ts +9 -13
  186. package/src/lib/framework/extension-api/input-component-extensions.tsx +8 -3
  187. package/src/lib/framework/extension-api/logic/alerts.ts +3 -2
  188. package/src/lib/framework/extension-api/types/alerts.ts +12 -6
  189. package/src/lib/framework/extension-api/types/data-table.ts +5 -2
  190. package/src/lib/framework/extension-api/types/login.ts +0 -21
  191. package/src/lib/framework/layout-engine/custom-form-page.stories.tsx +344 -0
  192. package/src/lib/framework/layout-engine/page-layout.tsx +11 -9
  193. package/src/lib/framework/layout-engine/page.stories.tsx +275 -0
  194. package/src/lib/framework/nav-menu/nav-menu-extensions.ts +32 -19
  195. package/src/lib/framework/page/detail-page.stories.tsx +151 -0
  196. package/src/lib/framework/page/list-page.stories.tsx +217 -0
  197. package/src/lib/framework/page/list-page.tsx +8 -1
  198. package/src/lib/graphql/api.ts +18 -1
  199. package/src/lib/graphql/graphql-env.d.ts +1 -1
  200. package/src/lib/hooks/use-alerts.ts +84 -0
  201. package/src/lib/hooks/use-floating-bulk-actions.ts +2 -3
  202. package/src/lib/index.ts +14 -1
  203. package/src/lib/providers/alerts-provider.tsx +60 -0
  204. package/src/lib/providers/theme-provider.tsx +6 -3
@@ -0,0 +1,344 @@
1
+ import { FormFieldWrapper } from '@/vdb/components/shared/form-field-wrapper.js';
2
+ import { Button } from '@/vdb/components/ui/button.js';
3
+ import { Input } from '@/vdb/components/ui/input.js';
4
+ import { Textarea } from '@/vdb/components/ui/textarea.js';
5
+ import { graphql } from '@/vdb/graphql/graphql.js';
6
+ import { Trans } from '@lingui/react/macro';
7
+ import type { Meta, StoryObj } from '@storybook/react-vite';
8
+ import { RouterContextProvider } from '@tanstack/react-router';
9
+ import { useForm } from 'react-hook-form';
10
+ import { createDemoRoute } from '../../../../.storybook/providers.js';
11
+ import {
12
+ DetailFormGrid,
13
+ Page,
14
+ PageActionBar,
15
+ PageActionBarRight,
16
+ PageBlock,
17
+ PageLayout,
18
+ PageTitle,
19
+ } from './page-layout.js';
20
+
21
+ // Sample GraphQL query for a product detail
22
+ const productFragment = graphql(`
23
+ fragment ProductDetailForForm on Product {
24
+ id
25
+ createdAt
26
+ updatedAt
27
+ name
28
+ slug
29
+ description
30
+ enabled
31
+ }
32
+ `);
33
+
34
+ const productQuery = graphql(
35
+ `
36
+ query ProductForCustomForm($id: ID!) {
37
+ product(id: $id) {
38
+ ...ProductDetailForForm
39
+ }
40
+ }
41
+ `,
42
+ [productFragment],
43
+ );
44
+
45
+ interface ProductFormData {
46
+ name: string;
47
+ slug: string;
48
+ description: string;
49
+ enabled: boolean;
50
+ }
51
+
52
+ const meta = {
53
+ title: 'Layout/Custom Form Page',
54
+ parameters: {
55
+ layout: 'fullscreen',
56
+ },
57
+ tags: ['autodocs'],
58
+ } satisfies Meta;
59
+
60
+ export default meta;
61
+ type Story = StoryObj<typeof meta>;
62
+
63
+ /**
64
+ * This example shows how to create a custom form page using FormFieldWrapper
65
+ * with a product entity. This pattern is useful when you want full control over
66
+ * the form layout instead of using the automated DetailPage component.
67
+ */
68
+ export const ProductCustomForm: Story = {
69
+ render: () => {
70
+ const { route, router } = createDemoRoute();
71
+ const form = useForm<ProductFormData>({
72
+ defaultValues: {
73
+ name: 'Wireless Headphones',
74
+ slug: 'wireless-headphones',
75
+ description: 'High-quality wireless headphones with active noise cancellation.',
76
+ enabled: true,
77
+ },
78
+ });
79
+
80
+ const onSubmit = (data: ProductFormData) => {
81
+ console.log('Form submitted:', data);
82
+ // In a real app, you would call your update mutation here
83
+ };
84
+
85
+ return (
86
+ <RouterContextProvider router={router}>
87
+ <Page
88
+ pageId="product-custom-detail"
89
+ form={form}
90
+ submitHandler={form.handleSubmit(onSubmit)}
91
+ entity={{
92
+ id: '1',
93
+ createdAt: '2024-01-01T00:00:00.000Z',
94
+ updatedAt: '2024-01-15T00:00:00.000Z',
95
+ }}
96
+ >
97
+ <PageTitle>
98
+ <Trans>Product: Wireless Headphones</Trans>
99
+ </PageTitle>
100
+ <PageActionBar>
101
+ <PageActionBarRight>
102
+ <Button type="submit" disabled={!form.formState.isDirty}>
103
+ <Trans>Save Changes</Trans>
104
+ </Button>
105
+ </PageActionBarRight>
106
+ </PageActionBar>
107
+ <PageLayout>
108
+ <PageBlock
109
+ column="main"
110
+ blockId="product-details"
111
+ title={<Trans>Product Details</Trans>}
112
+ description={<Trans>Basic information about the product</Trans>}
113
+ >
114
+ <DetailFormGrid>
115
+ <FormFieldWrapper
116
+ control={form.control}
117
+ name="name"
118
+ label={<Trans>Product Name</Trans>}
119
+ description={<Trans>The display name of the product</Trans>}
120
+ render={({ field }) => <Input {...field} />}
121
+ />
122
+ <FormFieldWrapper
123
+ control={form.control}
124
+ name="slug"
125
+ label={<Trans>Slug</Trans>}
126
+ description={<Trans>URL-friendly identifier</Trans>}
127
+ render={({ field }) => <Input {...field} />}
128
+ />
129
+ </DetailFormGrid>
130
+ <FormFieldWrapper
131
+ control={form.control}
132
+ name="description"
133
+ label={<Trans>Description</Trans>}
134
+ render={({ field }) => <Textarea {...field} rows={4} />}
135
+ />
136
+ </PageBlock>
137
+ <PageBlock column="side" blockId="product-status" title={<Trans>Status</Trans>}>
138
+ <FormFieldWrapper
139
+ control={form.control}
140
+ name="enabled"
141
+ label={<Trans>Enabled</Trans>}
142
+ description={<Trans>Whether this product is active</Trans>}
143
+ render={({ field }) => (
144
+ <div className="flex items-center">
145
+ <input
146
+ type="checkbox"
147
+ checked={field.value}
148
+ onChange={field.onChange}
149
+ className="mr-2"
150
+ />
151
+ <span className="text-sm">
152
+ {field.value ? (
153
+ <Trans>Product is enabled</Trans>
154
+ ) : (
155
+ <Trans>Product is disabled</Trans>
156
+ )}
157
+ </span>
158
+ </div>
159
+ )}
160
+ />
161
+ </PageBlock>
162
+ </PageLayout>
163
+ </Page>
164
+ </RouterContextProvider>
165
+ );
166
+ },
167
+ };
168
+
169
+ /**
170
+ * This example shows a more complex form with multiple blocks and varied form fields.
171
+ */
172
+ export const ComplexCustomForm: Story = {
173
+ render: () => {
174
+ const { route, router } = createDemoRoute();
175
+ const form = useForm({
176
+ defaultValues: {
177
+ // Basic Info
178
+ name: 'Premium Laptop',
179
+ slug: 'premium-laptop',
180
+ sku: 'LAPTOP-001',
181
+ // Pricing
182
+ price: 1299.99,
183
+ salePrice: null,
184
+ costPrice: 899.0,
185
+ // Inventory
186
+ stockOnHand: 50,
187
+ trackInventory: true,
188
+ // Details
189
+ description: 'High-performance laptop for professionals',
190
+ shortDescription: 'Professional laptop',
191
+ // SEO
192
+ metaTitle: '',
193
+ metaDescription: '',
194
+ },
195
+ });
196
+
197
+ const onSubmit = (data: any) => {
198
+ console.log('Form submitted:', data);
199
+ };
200
+
201
+ return (
202
+ <RouterContextProvider router={router}>
203
+ <Page
204
+ pageId="product-complex-detail"
205
+ form={form}
206
+ submitHandler={form.handleSubmit(onSubmit)}
207
+ entity={{ id: '2', createdAt: '2024-01-01', updatedAt: '2024-01-15' }}
208
+ >
209
+ <PageTitle>
210
+ <Trans>Product: Premium Laptop</Trans>
211
+ </PageTitle>
212
+ <PageActionBar>
213
+ <PageActionBarRight>
214
+ <Button variant="outline" type="button">
215
+ <Trans>Cancel</Trans>
216
+ </Button>
217
+ <Button type="submit" disabled={!form.formState.isDirty}>
218
+ <Trans>Save Changes</Trans>
219
+ </Button>
220
+ </PageActionBarRight>
221
+ </PageActionBar>
222
+ <PageLayout>
223
+ <PageBlock
224
+ column="main"
225
+ blockId="basic-info"
226
+ title={<Trans>Basic Information</Trans>}
227
+ >
228
+ <DetailFormGrid>
229
+ <FormFieldWrapper
230
+ control={form.control}
231
+ name="name"
232
+ label={<Trans>Product Name</Trans>}
233
+ render={({ field }) => <Input {...field} />}
234
+ />
235
+ <FormFieldWrapper
236
+ control={form.control}
237
+ name="sku"
238
+ label={<Trans>SKU</Trans>}
239
+ render={({ field }) => <Input {...field} />}
240
+ />
241
+ <FormFieldWrapper
242
+ control={form.control}
243
+ name="slug"
244
+ label={<Trans>Slug</Trans>}
245
+ render={({ field }) => <Input {...field} />}
246
+ />
247
+ </DetailFormGrid>
248
+ </PageBlock>
249
+
250
+ <PageBlock column="main" blockId="description" title={<Trans>Description</Trans>}>
251
+ <FormFieldWrapper
252
+ control={form.control}
253
+ name="shortDescription"
254
+ label={<Trans>Short Description</Trans>}
255
+ render={({ field }) => <Input {...field} />}
256
+ />
257
+ <FormFieldWrapper
258
+ control={form.control}
259
+ name="description"
260
+ label={<Trans>Full Description</Trans>}
261
+ render={({ field }) => <Textarea {...field} rows={6} />}
262
+ />
263
+ </PageBlock>
264
+
265
+ <PageBlock column="main" blockId="pricing" title={<Trans>Pricing</Trans>}>
266
+ <DetailFormGrid>
267
+ <FormFieldWrapper
268
+ control={form.control}
269
+ name="price"
270
+ label={<Trans>Price</Trans>}
271
+ render={({ field }) => <Input {...field} type="number" step="0.01" />}
272
+ />
273
+ <FormFieldWrapper
274
+ control={form.control}
275
+ name="salePrice"
276
+ label={<Trans>Sale Price</Trans>}
277
+ description={<Trans>Optional discounted price</Trans>}
278
+ render={({ field }) => (
279
+ <Input
280
+ {...field}
281
+ type="number"
282
+ step="0.01"
283
+ value={field.value ?? ''}
284
+ />
285
+ )}
286
+ />
287
+ <FormFieldWrapper
288
+ control={form.control}
289
+ name="costPrice"
290
+ label={<Trans>Cost Price</Trans>}
291
+ description={<Trans>Your cost for this product</Trans>}
292
+ render={({ field }) => <Input {...field} type="number" step="0.01" />}
293
+ />
294
+ </DetailFormGrid>
295
+ </PageBlock>
296
+
297
+ <PageBlock column="side" blockId="inventory" title={<Trans>Inventory</Trans>}>
298
+ <div className="space-y-4">
299
+ <FormFieldWrapper
300
+ control={form.control}
301
+ name="trackInventory"
302
+ label={<Trans>Track Inventory</Trans>}
303
+ render={({ field }) => (
304
+ <div className="flex items-center">
305
+ <input
306
+ type="checkbox"
307
+ checked={field.value}
308
+ onChange={field.onChange}
309
+ className="mr-2"
310
+ />
311
+ </div>
312
+ )}
313
+ />
314
+ <FormFieldWrapper
315
+ control={form.control}
316
+ name="stockOnHand"
317
+ label={<Trans>Stock on Hand</Trans>}
318
+ render={({ field }) => <Input {...field} type="number" />}
319
+ />
320
+ </div>
321
+ </PageBlock>
322
+
323
+ <PageBlock column="side" blockId="seo" title={<Trans>SEO</Trans>}>
324
+ <div className="space-y-4">
325
+ <FormFieldWrapper
326
+ control={form.control}
327
+ name="metaTitle"
328
+ label={<Trans>Meta Title</Trans>}
329
+ render={({ field }) => <Input {...field} />}
330
+ />
331
+ <FormFieldWrapper
332
+ control={form.control}
333
+ name="metaDescription"
334
+ label={<Trans>Meta Description</Trans>}
335
+ render={({ field }) => <Textarea {...field} rows={3} />}
336
+ />
337
+ </div>
338
+ </PageBlock>
339
+ </PageLayout>
340
+ </Page>
341
+ </RouterContextProvider>
342
+ );
343
+ },
344
+ };
@@ -276,17 +276,17 @@ export function PageLayout({ children, className }: Readonly<PageLayoutProps>) {
276
276
  const sideBlocks = finalChildArray.filter(child => isPageBlock(child) && child.props.column === 'side');
277
277
 
278
278
  return (
279
- <div className={cn('w-full space-y-4', className)}>
279
+ <div className={cn('w-full space-y-4', className, '@container/layout')}>
280
280
  {isDesktop ? (
281
- <div className="hidden md:grid md:grid-cols-5 lg:grid-cols-4 md:gap-4">
281
+ <div className="grid grid-cols-1 gap-4 @3xl/layout:grid-cols-4">
282
282
  {fullWidthBlocks.length > 0 && (
283
- <div className="md:col-span-5 space-y-4">{fullWidthBlocks}</div>
283
+ <div className="@md/layout:col-span-5 space-y-4">{fullWidthBlocks}</div>
284
284
  )}
285
- <div className="md:col-span-3 space-y-4">{mainBlocks}</div>
286
- <div className="md:col-span-2 lg:col-span-1 space-y-4">{sideBlocks}</div>
285
+ <div className="@3xl/layout:col-span-3 space-y-4">{mainBlocks}</div>
286
+ <div className="@3xl/layout:col-span-1 space-y-4">{sideBlocks}</div>
287
287
  </div>
288
288
  ) : (
289
- <div className="md:hidden space-y-4">{children}</div>
289
+ <div className="space-y-4">{children}</div>
290
290
  )}
291
291
  </div>
292
292
  );
@@ -579,14 +579,16 @@ export function PageBlock({
579
579
  return (
580
580
  <PageBlockContext.Provider value={contextValue}>
581
581
  <LocationWrapper>
582
- <Card className={cn('@container w-full', className)}>
582
+ <Card className={cn('@container w-full', className, 'animate-in fade-in duration-300')}>
583
583
  {title || description ? (
584
584
  <CardHeader>
585
585
  {title && <CardTitle>{title}</CardTitle>}
586
586
  {description && <CardDescription>{description}</CardDescription>}
587
587
  </CardHeader>
588
588
  ) : null}
589
- <CardContent className={cn(!title ? 'pt-6' : '')}>{children}</CardContent>
589
+ <CardContent className={cn(!title ? 'pt-6' : '', 'overflow-auto')}>
590
+ {children}
591
+ </CardContent>
590
592
  </Card>
591
593
  </LocationWrapper>
592
594
  </PageBlockContext.Provider>
@@ -613,7 +615,7 @@ export function FullWidthPageBlock({
613
615
  return (
614
616
  <PageBlockContext.Provider value={contextValue}>
615
617
  <LocationWrapper>
616
- <div className={cn('w-full', className)}>{children}</div>
618
+ <div className={cn('w-full', className, 'animate-in fade-in duration-300')}>{children}</div>
617
619
  </LocationWrapper>
618
620
  </PageBlockContext.Provider>
619
621
  );
@@ -0,0 +1,275 @@
1
+ import { Button } from '@/vdb/components/ui/button.js';
2
+ import type { Meta, StoryObj } from '@storybook/react-vite';
3
+ import { RouterContextProvider } from '@tanstack/react-router';
4
+ import { createDemoRoute } from '../../../../.storybook/providers.js';
5
+ import {
6
+ FullWidthPageBlock,
7
+ Page,
8
+ PageActionBar,
9
+ PageActionBarLeft,
10
+ PageActionBarRight,
11
+ PageBlock,
12
+ PageLayout,
13
+ PageTitle,
14
+ } from './page-layout.js';
15
+
16
+ const meta = {
17
+ title: 'Layout/Page Layout',
18
+ component: Page,
19
+ parameters: {
20
+ layout: 'fullscreen',
21
+ },
22
+ tags: ['autodocs'],
23
+ } satisfies Meta<typeof Page>;
24
+
25
+ export default meta;
26
+ type Story = StoryObj<typeof meta>;
27
+
28
+ export const Playground: Story = {
29
+ render: () => {
30
+ const { route, router } = createDemoRoute();
31
+ return (
32
+ <RouterContextProvider router={router}>
33
+ <Page pageId="test-page">
34
+ <PageTitle>Test Page</PageTitle>
35
+ <PageLayout>
36
+ <PageBlock column="main" blockId="main-stuff">
37
+ This will display in the main area
38
+ </PageBlock>
39
+ <PageBlock column="side" blockId="side-stuff">
40
+ This will display in the side area
41
+ </PageBlock>
42
+ </PageLayout>
43
+ </Page>
44
+ </RouterContextProvider>
45
+ );
46
+ },
47
+ };
48
+
49
+ export const WithActionBar: Story = {
50
+ render: () => {
51
+ const { route, router } = createDemoRoute();
52
+ return (
53
+ <RouterContextProvider router={router}>
54
+ <Page pageId="product-detail">
55
+ <PageTitle>Product Details</PageTitle>
56
+ <PageActionBar>
57
+ <PageActionBarLeft>
58
+ <Button variant="outline">Cancel</Button>
59
+ </PageActionBarLeft>
60
+ <PageActionBarRight>
61
+ <Button>Save</Button>
62
+ </PageActionBarRight>
63
+ </PageActionBar>
64
+ <PageLayout>
65
+ <PageBlock column="main" blockId="product-info" title="Product Information">
66
+ <div className="space-y-4">
67
+ <div>
68
+ <label className="text-sm font-medium">Name</label>
69
+ <input
70
+ type="text"
71
+ className="w-full border rounded px-3 py-2 mt-1"
72
+ defaultValue="Wireless Headphones"
73
+ />
74
+ </div>
75
+ <div>
76
+ <label className="text-sm font-medium">Description</label>
77
+ <textarea
78
+ className="w-full border rounded px-3 py-2 mt-1"
79
+ rows={4}
80
+ defaultValue="High-quality wireless headphones with active noise cancellation."
81
+ />
82
+ </div>
83
+ </div>
84
+ </PageBlock>
85
+ <PageBlock column="side" blockId="product-meta" title="Metadata">
86
+ <div className="space-y-3">
87
+ <div>
88
+ <div className="text-sm font-medium">Status</div>
89
+ <div className="text-sm text-muted-foreground">Active</div>
90
+ </div>
91
+ <div>
92
+ <div className="text-sm font-medium">SKU</div>
93
+ <div className="text-sm text-muted-foreground">WH-001</div>
94
+ </div>
95
+ <div>
96
+ <div className="text-sm font-medium">Price</div>
97
+ <div className="text-sm text-muted-foreground">$299.00</div>
98
+ </div>
99
+ </div>
100
+ </PageBlock>
101
+ </PageLayout>
102
+ </Page>
103
+ </RouterContextProvider>
104
+ );
105
+ },
106
+ };
107
+
108
+ export const MultipleBlocks: Story = {
109
+ render: () => {
110
+ const { route, router } = createDemoRoute();
111
+ return (
112
+ <RouterContextProvider router={router}>
113
+ <Page pageId="complex-page">
114
+ <PageTitle>Complex Page Layout</PageTitle>
115
+ <PageLayout>
116
+ <PageBlock
117
+ column="main"
118
+ blockId="block-1"
119
+ title="Main Block 1"
120
+ description="This is the first main block"
121
+ >
122
+ <p>Content for the first main block goes here.</p>
123
+ </PageBlock>
124
+ <PageBlock
125
+ column="main"
126
+ blockId="block-2"
127
+ title="Main Block 2"
128
+ description="This is the second main block"
129
+ >
130
+ <p>Content for the second main block goes here.</p>
131
+ </PageBlock>
132
+ <PageBlock column="side" blockId="side-1" title="Sidebar Block 1">
133
+ <p>First sidebar block content.</p>
134
+ </PageBlock>
135
+ <PageBlock column="side" blockId="side-2" title="Sidebar Block 2">
136
+ <p>Second sidebar block content.</p>
137
+ </PageBlock>
138
+ <PageBlock column="side" blockId="side-3" title="Sidebar Block 3">
139
+ <p>Third sidebar block content.</p>
140
+ </PageBlock>
141
+ </PageLayout>
142
+ </Page>
143
+ </RouterContextProvider>
144
+ );
145
+ },
146
+ };
147
+
148
+ export const WithFullWidthBlock: Story = {
149
+ render: () => {
150
+ const { route, router } = createDemoRoute();
151
+ return (
152
+ <RouterContextProvider router={router}>
153
+ <Page pageId="dashboard-overview">
154
+ <PageTitle>Dashboard Overview</PageTitle>
155
+ <PageLayout>
156
+ <FullWidthPageBlock blockId="stats">
157
+ <div className="grid grid-cols-1 md:grid-cols-4 gap-4 p-6 bg-muted/50 rounded-lg">
158
+ <div className="text-center">
159
+ <div className="text-3xl font-bold">1,234</div>
160
+ <div className="text-sm text-muted-foreground">Total Orders</div>
161
+ </div>
162
+ <div className="text-center">
163
+ <div className="text-3xl font-bold">$45,678</div>
164
+ <div className="text-sm text-muted-foreground">Revenue</div>
165
+ </div>
166
+ <div className="text-center">
167
+ <div className="text-3xl font-bold">567</div>
168
+ <div className="text-sm text-muted-foreground">Products</div>
169
+ </div>
170
+ <div className="text-center">
171
+ <div className="text-3xl font-bold">890</div>
172
+ <div className="text-sm text-muted-foreground">Customers</div>
173
+ </div>
174
+ </div>
175
+ </FullWidthPageBlock>
176
+ <PageBlock column="main" blockId="recent-orders" title="Recent Orders">
177
+ <div className="space-y-2">
178
+ {[1, 2, 3].map(i => (
179
+ <div key={i} className="flex justify-between py-2 border-b">
180
+ <span>Order #{1000 + i}</span>
181
+ <span className="text-muted-foreground">$99.00</span>
182
+ </div>
183
+ ))}
184
+ </div>
185
+ </PageBlock>
186
+ <PageBlock column="side" blockId="quick-stats" title="Quick Stats">
187
+ <div className="space-y-3">
188
+ <div>
189
+ <div className="text-sm font-medium">Pending Orders</div>
190
+ <div className="text-2xl font-bold">12</div>
191
+ </div>
192
+ <div>
193
+ <div className="text-sm font-medium">Low Stock Items</div>
194
+ <div className="text-2xl font-bold">5</div>
195
+ </div>
196
+ </div>
197
+ </PageBlock>
198
+ </PageLayout>
199
+ </Page>
200
+ </RouterContextProvider>
201
+ );
202
+ },
203
+ };
204
+
205
+ export const MinimalPage: Story = {
206
+ render: () => {
207
+ const { route, router } = createDemoRoute();
208
+ return (
209
+ <RouterContextProvider router={router}>
210
+ <Page pageId="simple-page">
211
+ <PageTitle>Simple Page</PageTitle>
212
+ <PageLayout>
213
+ <PageBlock column="main" blockId="content">
214
+ <p>This is a minimal page with just a title and one content block.</p>
215
+ </PageBlock>
216
+ </PageLayout>
217
+ </Page>
218
+ </RouterContextProvider>
219
+ );
220
+ },
221
+ };
222
+
223
+ export const WithBlockDescriptions: Story = {
224
+ render: () => {
225
+ const { route, router } = createDemoRoute();
226
+ return (
227
+ <RouterContextProvider router={router}>
228
+ <Page pageId="settings-page">
229
+ <PageTitle>Settings</PageTitle>
230
+ <PageLayout>
231
+ <PageBlock
232
+ column="main"
233
+ blockId="general"
234
+ title="General Settings"
235
+ description="Configure general application settings and preferences"
236
+ >
237
+ <div className="space-y-4">
238
+ <div className="flex items-center justify-between">
239
+ <label className="text-sm font-medium">Enable notifications</label>
240
+ <input type="checkbox" defaultChecked />
241
+ </div>
242
+ <div className="flex items-center justify-between">
243
+ <label className="text-sm font-medium">Dark mode</label>
244
+ <input type="checkbox" />
245
+ </div>
246
+ </div>
247
+ </PageBlock>
248
+ <PageBlock
249
+ column="main"
250
+ blockId="advanced"
251
+ title="Advanced Settings"
252
+ description="Advanced configuration options for power users"
253
+ >
254
+ <div className="space-y-4">
255
+ <div>
256
+ <label className="text-sm font-medium">API Key</label>
257
+ <input
258
+ type="text"
259
+ className="w-full border rounded px-3 py-2 mt-1"
260
+ defaultValue="sk_test_..."
261
+ />
262
+ </div>
263
+ </div>
264
+ </PageBlock>
265
+ <PageBlock column="side" blockId="help" title="Help" description="Need assistance?">
266
+ <Button variant="outline" className="w-full">
267
+ View Documentation
268
+ </Button>
269
+ </PageBlock>
270
+ </PageLayout>
271
+ </Page>
272
+ </RouterContextProvider>
273
+ );
274
+ },
275
+ };