@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.
- package/dist/plugin/dashboard.plugin.js +1 -1
- package/dist/vite/utils/ast-utils.spec.js +3 -3
- package/dist/vite/vite-plugin-hmr.d.ts +8 -0
- package/dist/vite/vite-plugin-hmr.js +34 -0
- package/dist/vite/vite-plugin-theme.js +6 -6
- package/dist/vite/vite-plugin-transform-index.js +6 -1
- package/dist/vite/vite-plugin-vendure-dashboard.d.ts +31 -4
- package/dist/vite/vite-plugin-vendure-dashboard.js +89 -34
- package/package.json +17 -5
- package/src/app/app-providers.tsx +4 -1
- package/src/app/common/map-faceted-filter-fields.ts +21 -0
- package/src/app/main.tsx +3 -1
- package/src/app/routes/_authenticated/_administrators/administrators.graphql.ts +2 -2
- package/src/app/routes/_authenticated/_administrators/administrators.tsx +13 -3
- package/src/app/routes/_authenticated/_administrators/administrators_.$id.tsx +6 -13
- package/src/app/routes/_authenticated/_administrators/components/role-permissions-display.tsx +1 -1
- package/src/app/routes/_authenticated/_assets/assets.tsx +17 -1
- package/src/app/routes/_authenticated/_collections/collections.graphql.ts +1 -0
- package/src/app/routes/_authenticated/_collections/collections.tsx +5 -0
- package/src/app/routes/_authenticated/_collections/components/collection-bulk-actions.tsx +0 -1
- package/src/app/routes/_authenticated/_customers/customers.tsx +9 -5
- package/src/app/routes/_authenticated/_facets/components/facet-bulk-actions.tsx +0 -6
- package/src/app/routes/_authenticated/_facets/components/facet-value-bulk-actions.tsx +16 -0
- package/src/app/routes/_authenticated/_facets/components/facet-values-table.tsx +43 -12
- package/src/app/routes/_authenticated/_facets/facets_.$facetId.values_.$id.tsx +14 -5
- package/src/app/routes/_authenticated/_orders/components/edit-order-table.tsx +117 -92
- package/src/app/routes/_authenticated/_orders/components/order-detail-shared.tsx +1 -1
- package/src/app/routes/_authenticated/_orders/components/order-modification-summary.tsx +2 -1
- package/src/app/routes/_authenticated/_orders/components/order-table-totals.tsx +26 -27
- package/src/app/routes/_authenticated/_orders/components/order-table.tsx +5 -3
- package/src/app/routes/_authenticated/_orders/components/state-transition-control.tsx +6 -9
- package/src/app/routes/_authenticated/_orders/orders.graphql.ts +17 -1
- package/src/app/routes/_authenticated/_orders/orders.tsx +2 -0
- package/src/app/routes/_authenticated/_orders/orders_.$id_.modify.tsx +48 -281
- package/src/app/routes/_authenticated/_orders/orders_.draft.$id.tsx +59 -40
- package/src/app/routes/_authenticated/_orders/utils/order-utils.ts +73 -0
- package/src/app/routes/_authenticated/_orders/utils/use-modify-order.ts +312 -0
- package/src/app/routes/_authenticated/_payment-methods/payment-methods.graphql.ts +2 -2
- package/src/app/routes/_authenticated/_payment-methods/payment-methods.tsx +4 -0
- package/src/app/routes/_authenticated/_product-variants/product-variants.tsx +2 -0
- package/src/app/routes/_authenticated/_products/components/product-bulk-actions.tsx +0 -6
- package/src/app/routes/_authenticated/_products/products.tsx +6 -2
- package/src/app/routes/_authenticated/_products/products_.$productId.option-groups.$productOptionGroupId.options_.$id.tsx +4 -8
- package/src/app/routes/_authenticated/_promotions/components/promotion-bulk-actions.tsx +0 -10
- package/src/app/routes/_authenticated/_promotions/promotions.graphql.ts +2 -2
- package/src/app/routes/_authenticated/_promotions/promotions.tsx +12 -0
- package/src/app/routes/_authenticated/_promotions/promotions_.$id.tsx +6 -2
- package/src/app/routes/_authenticated/_sellers/sellers.graphql.ts +2 -2
- package/src/app/routes/_authenticated/_shipping-methods/shipping-methods.graphql.ts +2 -2
- package/src/app/routes/_authenticated/_shipping-methods/shipping-methods.tsx +4 -0
- package/src/app/routes/_authenticated/_shipping-methods/shipping-methods_.$id.tsx +4 -10
- package/src/app/routes/_authenticated/_stock-locations/stock-locations.graphql.ts +2 -2
- package/src/app/routes/_authenticated/_tax-categories/tax-categories.graphql.ts +2 -2
- package/src/app/routes/_authenticated/_tax-rates/tax-rates.tsx +9 -0
- package/src/app/routes/_authenticated/_tax-rates/tax-rates_.$id.tsx +1 -0
- package/src/app/routes/_authenticated/_zones/zones.graphql.ts +2 -2
- package/src/app/routes/login.tsx +2 -2
- package/src/i18n/locales/ar.po +420 -289
- package/src/i18n/locales/cs.po +420 -289
- package/src/i18n/locales/de.po +420 -289
- package/src/i18n/locales/en.po +420 -289
- package/src/i18n/locales/es.po +420 -289
- package/src/i18n/locales/fa.po +420 -289
- package/src/i18n/locales/fr.po +468 -337
- package/src/i18n/locales/he.po +420 -289
- package/src/i18n/locales/hr.po +420 -289
- package/src/i18n/locales/it.po +420 -289
- package/src/i18n/locales/ja.po +420 -289
- package/src/i18n/locales/nb.po +420 -289
- package/src/i18n/locales/ne.po +420 -289
- package/src/i18n/locales/pl.po +420 -289
- package/src/i18n/locales/pt_BR.po +420 -289
- package/src/i18n/locales/pt_PT.po +420 -289
- package/src/i18n/locales/ru.po +420 -289
- package/src/i18n/locales/sv.po +420 -289
- package/src/i18n/locales/tr.po +420 -289
- package/src/i18n/locales/uk.po +420 -289
- package/src/i18n/locales/zh_Hans.po +420 -289
- package/src/i18n/locales/zh_Hant.po +420 -289
- package/src/lib/components/data-input/affixed-input.stories.tsx +93 -0
- package/src/lib/components/data-input/affixed-input.tsx +5 -2
- package/src/lib/components/data-input/boolean-input.stories.tsx +102 -0
- package/src/lib/components/data-input/checkbox-input.stories.tsx +61 -0
- package/src/lib/components/data-input/datetime-input.stories.tsx +62 -0
- package/src/lib/components/data-input/datetime-input.tsx +27 -13
- package/src/lib/components/data-input/default-relation-input.tsx +18 -12
- package/src/lib/components/data-input/money-input.stories.tsx +88 -0
- package/src/lib/components/data-input/number-input.stories.tsx +103 -0
- package/src/lib/components/data-input/number-input.tsx +10 -4
- package/src/lib/components/data-input/password-form-input.stories.tsx +65 -0
- package/src/lib/components/data-input/{password-input.tsx → password-form-input.tsx} +1 -1
- package/src/lib/components/data-input/rich-text-input.stories.tsx +92 -0
- package/src/lib/components/data-input/slug-input.stories.tsx +232 -0
- package/src/lib/components/data-input/slug-input.tsx +9 -10
- package/src/lib/components/data-input/text-input.stories.tsx +52 -0
- package/src/lib/components/data-input/textarea-input.stories.tsx +55 -0
- package/src/lib/components/data-table/add-filter-menu.tsx +6 -1
- package/src/lib/components/data-table/column-header-wrapper.tsx +106 -0
- package/src/lib/components/data-table/data-table-bulk-action-item.tsx +11 -9
- package/src/lib/components/data-table/data-table-bulk-actions.tsx +4 -4
- package/src/lib/components/data-table/data-table-column-header.tsx +17 -14
- package/src/lib/components/data-table/data-table-faceted-filter.tsx +33 -11
- package/src/lib/components/data-table/data-table-filter-badge-editable.tsx +35 -0
- package/src/lib/components/data-table/data-table-filter-badge.tsx +23 -16
- package/src/lib/components/data-table/data-table-filter-dialog.tsx +28 -8
- package/src/lib/components/data-table/data-table-pagination.tsx +23 -7
- package/src/lib/components/data-table/data-table.stories.tsx +249 -0
- package/src/lib/components/data-table/data-table.tsx +37 -9
- package/src/lib/components/data-table/filters/data-table-datetime-filter.tsx +79 -34
- package/src/lib/components/data-table/use-generated-columns.tsx +55 -27
- package/src/lib/components/layout/nav-user.tsx +19 -13
- package/src/lib/components/login/login-form.tsx +39 -123
- package/src/lib/components/shared/alerts.tsx +29 -17
- package/src/lib/components/shared/asset/asset-bulk-actions.tsx +3 -3
- package/src/lib/components/shared/asset/asset-gallery.stories.tsx +76 -0
- package/src/lib/components/shared/asset/asset-gallery.tsx +147 -113
- package/src/lib/components/shared/asset/asset-picker-dialog.stories.tsx +58 -0
- package/src/lib/components/shared/customer-group-selector.tsx +5 -2
- package/src/lib/components/shared/detail-page-button.stories.tsx +52 -0
- package/src/lib/components/shared/facet-value-selector.stories.tsx +48 -0
- package/src/lib/components/shared/facet-value-selector.tsx +130 -34
- package/src/lib/components/shared/paginated-list-data-table.stories.tsx +212 -0
- package/src/lib/components/shared/paginated-list-data-table.tsx +12 -12
- package/src/lib/components/shared/permission-guard.stories.tsx +46 -0
- package/src/lib/components/shared/remove-from-channel-bulk-action.tsx +2 -0
- package/src/lib/components/shared/rich-text-editor/responsive-toolbar.tsx +8 -4
- package/src/lib/components/shared/rich-text-editor/rich-text-editor.tsx +1 -0
- package/src/lib/components/shared/table-cell/order-table-cell-components.tsx +40 -0
- package/src/lib/components/shared/vendure-image.stories.tsx +167 -0
- package/src/lib/components/shared/vendure-image.tsx +6 -7
- package/src/lib/components/ui/accordion.stories.tsx +33 -0
- package/src/lib/components/ui/alert-dialog.stories.tsx +48 -0
- package/src/lib/components/ui/alert.stories.tsx +35 -0
- package/src/lib/components/ui/aspect-ratio.stories.tsx +28 -0
- package/src/lib/components/ui/badge.stories.tsx +28 -0
- package/src/lib/components/ui/breadcrumb.stories.tsx +41 -0
- package/src/lib/components/ui/button.stories.tsx +38 -0
- package/src/lib/components/ui/calendar.stories.tsx +22 -0
- package/src/lib/components/ui/card.stories.tsx +28 -0
- package/src/lib/components/ui/carousel.stories.tsx +34 -0
- package/src/lib/components/ui/checkbox.stories.tsx +31 -0
- package/src/lib/components/ui/collapsible.stories.tsx +39 -0
- package/src/lib/components/ui/command.stories.tsx +44 -0
- package/src/lib/components/ui/context-menu.stories.tsx +38 -0
- package/src/lib/components/ui/dialog.stories.tsx +52 -0
- package/src/lib/components/ui/drawer.stories.tsx +50 -0
- package/src/lib/components/ui/dropdown-menu.stories.tsx +41 -0
- package/src/lib/components/ui/hover-card.stories.tsx +38 -0
- package/src/lib/components/ui/input-group.tsx +148 -0
- package/src/lib/components/ui/input-otp.stories.tsx +30 -0
- package/src/lib/components/ui/input.stories.tsx +38 -0
- package/src/lib/components/ui/label.stories.tsx +24 -0
- package/src/lib/components/ui/menubar.stories.tsx +53 -0
- package/src/lib/components/ui/navigation-menu.stories.tsx +54 -0
- package/src/lib/components/ui/pagination.stories.tsx +51 -0
- package/src/lib/components/ui/password-input.stories.tsx +32 -0
- package/src/lib/components/ui/password-input.tsx +29 -0
- package/src/lib/components/ui/popover.stories.tsx +33 -0
- package/src/lib/components/ui/progress.stories.tsx +27 -0
- package/src/lib/components/ui/radio-group.stories.tsx +34 -0
- package/src/lib/components/ui/resizable.stories.tsx +32 -0
- package/src/lib/components/ui/scroll-area.stories.tsx +31 -0
- package/src/lib/components/ui/select.stories.tsx +36 -0
- package/src/lib/components/ui/separator.stories.tsx +35 -0
- package/src/lib/components/ui/sheet.stories.tsx +50 -0
- package/src/lib/components/ui/sidebar-context.ts +16 -0
- package/src/lib/components/ui/sidebar.tsx +2 -13
- package/src/lib/components/ui/skeleton.stories.tsx +26 -0
- package/src/lib/components/ui/slider.stories.tsx +37 -0
- package/src/lib/components/ui/switch.stories.tsx +31 -0
- package/src/lib/components/ui/table.stories.tsx +52 -0
- package/src/lib/components/ui/tabs.stories.tsx +29 -0
- package/src/lib/components/ui/textarea.stories.tsx +32 -0
- package/src/lib/components/ui/toggle-group.stories.tsx +31 -0
- package/src/lib/components/ui/toggle.stories.tsx +39 -0
- package/src/lib/components/ui/tooltip.stories.tsx +30 -0
- package/src/lib/components/ui/tooltip.tsx +2 -2
- package/src/lib/framework/alert/alert-extensions.tsx +0 -11
- package/src/lib/framework/alert/alert-item.tsx +14 -19
- package/src/lib/framework/alert/alerts-indicator.tsx +14 -15
- package/src/lib/framework/alert/search-index-buffer-alert/search-index-buffer-alert.ts +41 -0
- package/src/lib/framework/component-registry/component-registry.tsx +3 -14
- package/src/lib/framework/dashboard-widget/base-widget.tsx +18 -9
- package/src/lib/framework/dashboard-widget/widget-filters-context.tsx +12 -11
- package/src/lib/framework/defaults.ts +9 -13
- package/src/lib/framework/extension-api/input-component-extensions.tsx +8 -3
- package/src/lib/framework/extension-api/logic/alerts.ts +3 -2
- package/src/lib/framework/extension-api/types/alerts.ts +12 -6
- package/src/lib/framework/extension-api/types/data-table.ts +5 -2
- package/src/lib/framework/extension-api/types/login.ts +0 -21
- package/src/lib/framework/layout-engine/custom-form-page.stories.tsx +344 -0
- package/src/lib/framework/layout-engine/page-layout.tsx +11 -9
- package/src/lib/framework/layout-engine/page.stories.tsx +275 -0
- package/src/lib/framework/nav-menu/nav-menu-extensions.ts +32 -19
- package/src/lib/framework/page/detail-page.stories.tsx +151 -0
- package/src/lib/framework/page/list-page.stories.tsx +217 -0
- package/src/lib/framework/page/list-page.tsx +8 -1
- package/src/lib/graphql/api.ts +18 -1
- package/src/lib/graphql/graphql-env.d.ts +1 -1
- package/src/lib/hooks/use-alerts.ts +84 -0
- package/src/lib/hooks/use-floating-bulk-actions.ts +2 -3
- package/src/lib/index.ts +14 -1
- package/src/lib/providers/alerts-provider.tsx +60 -0
- 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="
|
|
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="
|
|
286
|
-
<div className="
|
|
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="
|
|
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' : '')}>
|
|
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
|
+
};
|