@standhigher/puck-page-builder 0.9.0 → 0.10.0
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/README.md +25 -0
- package/dist/{chunk-PUTYZI7B.js → chunk-PFCWXLWN.js} +117 -2
- package/dist/extensions.d.ts +18 -183
- package/dist/extensions.js +15 -3
- package/dist/index.d.ts +199 -9
- package/dist/index.js +477 -143
- package/dist/registry-eNtkEC6o.d.ts +221 -0
- package/dist/renderer.d.ts +1 -1
- package/dist/runtime.d.ts +2 -1
- package/dist/runtime.js +15 -3
- package/package.json +1 -1
- package/src/styles.css +12 -1
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { ComponentType, ReactNode } from 'react';
|
|
2
|
+
import { RenderTarget, JsonValue, PageDocument } from './schema.js';
|
|
3
|
+
import { ThemeTokens } from './theme.js';
|
|
4
|
+
|
|
5
|
+
type ExtensionTarget = RenderTarget | "all";
|
|
6
|
+
type EditorActionPosition = "left" | "center" | "right";
|
|
7
|
+
type UISlotName = "toolbar.left" | "toolbar.center" | "toolbar.right" | "inspector.content";
|
|
8
|
+
type ValidationIssue = {
|
|
9
|
+
path: string;
|
|
10
|
+
message: string;
|
|
11
|
+
};
|
|
12
|
+
/** Operations that can be enabled globally or constrained for one block type. */
|
|
13
|
+
type BlockOperation = "add" | "delete" | "duplicate" | "drag";
|
|
14
|
+
/**
|
|
15
|
+
* Reusable limits for a block type.
|
|
16
|
+
*
|
|
17
|
+
* `required` is shorthand for `minInstances: 1`; `singleton` is shorthand for
|
|
18
|
+
* `maxInstances: 1`. Explicit numeric limits can be used when a page needs
|
|
19
|
+
* more than one required instance.
|
|
20
|
+
*/
|
|
21
|
+
type BlockPolicy = Partial<Record<`allow${Capitalize<BlockOperation>}`, boolean>> & {
|
|
22
|
+
required?: boolean;
|
|
23
|
+
singleton?: boolean;
|
|
24
|
+
minInstances?: number;
|
|
25
|
+
maxInstances?: number;
|
|
26
|
+
};
|
|
27
|
+
/** Global editor switches. Per-block policies can only further specialize them. */
|
|
28
|
+
type EditorOperationPolicy = Partial<Record<`allow${Capitalize<BlockOperation>}`, boolean>>;
|
|
29
|
+
/**
|
|
30
|
+
* Field validation for string controls. URL controls accept same-origin paths
|
|
31
|
+
* by default and HTTP(S) URLs; color controls accept CSS hexadecimal colors.
|
|
32
|
+
*/
|
|
33
|
+
type FieldValidation = {
|
|
34
|
+
minLength?: number;
|
|
35
|
+
maxLength?: number;
|
|
36
|
+
allowRelativeUrl?: boolean;
|
|
37
|
+
/** Allows HTTP only for an explicit local development host. */
|
|
38
|
+
allowLocalhost?: boolean;
|
|
39
|
+
allowedUrlProtocols?: Array<"http:" | "https:">;
|
|
40
|
+
};
|
|
41
|
+
type FieldConfig = {
|
|
42
|
+
field: string;
|
|
43
|
+
label?: string;
|
|
44
|
+
/** Product-facing editor metadata. Custom field components remain supported. */
|
|
45
|
+
control?: "text" | "textarea" | "url" | "color";
|
|
46
|
+
description?: string;
|
|
47
|
+
group?: string;
|
|
48
|
+
/** Kept for compatibility; it participates in the built-in validation. */
|
|
49
|
+
required?: boolean;
|
|
50
|
+
validation?: FieldValidation;
|
|
51
|
+
};
|
|
52
|
+
type FieldProps<T = unknown> = {
|
|
53
|
+
value: T;
|
|
54
|
+
onChange(value: T): void;
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* Props available only to an extension's editor-canvas renderer.
|
|
58
|
+
* They are separate from Web rendering so editor interaction cannot invoke a
|
|
59
|
+
* host Runtime or become persisted operational data.
|
|
60
|
+
*/
|
|
61
|
+
type BlockEditorProps<P = Record<string, unknown>> = P & {
|
|
62
|
+
blockId: string;
|
|
63
|
+
selected: boolean;
|
|
64
|
+
onPropsChange(props: Record<string, JsonValue>): void;
|
|
65
|
+
};
|
|
66
|
+
interface BlockDefinition<P = Record<string, unknown>> {
|
|
67
|
+
type: string;
|
|
68
|
+
version: number;
|
|
69
|
+
label: string;
|
|
70
|
+
category: string;
|
|
71
|
+
targets: ExtensionTarget[];
|
|
72
|
+
defaultProps: P;
|
|
73
|
+
fields: Record<string, FieldConfig>;
|
|
74
|
+
render: Partial<Record<RenderTarget, ComponentType<P>>> & {
|
|
75
|
+
/** Optional edit-mode renderer. WebRenderer never uses this surface. */
|
|
76
|
+
editor?: ComponentType<BlockEditorProps<P>>;
|
|
77
|
+
};
|
|
78
|
+
defaultVariant?: string;
|
|
79
|
+
variants?: BlockVariantDefinition[];
|
|
80
|
+
/** Default interaction and cardinality rules for this block type. */
|
|
81
|
+
policy?: BlockPolicy;
|
|
82
|
+
dataSources?: string[];
|
|
83
|
+
validate?: (props: P) => ValidationIssue[];
|
|
84
|
+
}
|
|
85
|
+
interface BlockVariantDefinition {
|
|
86
|
+
id: string;
|
|
87
|
+
label: string;
|
|
88
|
+
theme?: ThemeTokens;
|
|
89
|
+
}
|
|
90
|
+
interface FieldDefinition<T = unknown> {
|
|
91
|
+
type: string;
|
|
92
|
+
component: ComponentType<FieldProps<T>>;
|
|
93
|
+
normalize?: (value: T) => T;
|
|
94
|
+
validate?: (value: T) => ValidationIssue[];
|
|
95
|
+
}
|
|
96
|
+
type ExtensionActionContext = {
|
|
97
|
+
document: PageDocument;
|
|
98
|
+
pageId: string;
|
|
99
|
+
notify?(message: string): void;
|
|
100
|
+
};
|
|
101
|
+
interface EditorAction {
|
|
102
|
+
id: string;
|
|
103
|
+
label: string;
|
|
104
|
+
position: EditorActionPosition;
|
|
105
|
+
order?: number;
|
|
106
|
+
variant?: "default" | "primary" | "danger";
|
|
107
|
+
hidden?: (context: ExtensionActionContext) => boolean;
|
|
108
|
+
disabled?: (context: ExtensionActionContext) => boolean;
|
|
109
|
+
execute(context: ExtensionActionContext): Promise<void> | void;
|
|
110
|
+
}
|
|
111
|
+
interface RendererDefinition {
|
|
112
|
+
id: string;
|
|
113
|
+
target: RenderTarget;
|
|
114
|
+
render(document: PageDocument): ReactNode | string;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* `P` defaults to `any` so an extension may contribute a narrower, validated
|
|
118
|
+
* parameter type while the framework-independent registry remains generic.
|
|
119
|
+
*/
|
|
120
|
+
interface DataSourceDefinition<P = any, R = unknown> {
|
|
121
|
+
key: string;
|
|
122
|
+
mock: (params: P) => Promise<R>;
|
|
123
|
+
live: (params: P) => Promise<R>;
|
|
124
|
+
validateParams?: (params: P) => ValidationIssue[];
|
|
125
|
+
}
|
|
126
|
+
type TemplateSource = "built-in" | "marketplace" | "custom";
|
|
127
|
+
interface TemplateDefinition {
|
|
128
|
+
id: string;
|
|
129
|
+
version: number;
|
|
130
|
+
name: string;
|
|
131
|
+
target: RenderTarget;
|
|
132
|
+
source: TemplateSource;
|
|
133
|
+
thumbnail?: string;
|
|
134
|
+
create(): PageDocument;
|
|
135
|
+
/** Block types that must be registered before this template can be used. */
|
|
136
|
+
requiredBlocks?: string[];
|
|
137
|
+
theme?: ThemeTokens;
|
|
138
|
+
}
|
|
139
|
+
interface LifecycleHooks {
|
|
140
|
+
onChange?(document: PageDocument): void;
|
|
141
|
+
beforeSave?(document: PageDocument): Promise<PageDocument>;
|
|
142
|
+
afterSave?(result: unknown): Promise<void>;
|
|
143
|
+
beforePublish?(document: PageDocument): Promise<PageDocument>;
|
|
144
|
+
afterPublish?(result: unknown): Promise<void>;
|
|
145
|
+
onError?(error: Error): void;
|
|
146
|
+
}
|
|
147
|
+
interface UISlotContribution {
|
|
148
|
+
id: string;
|
|
149
|
+
slot: UISlotName;
|
|
150
|
+
order?: number;
|
|
151
|
+
component: ComponentType;
|
|
152
|
+
}
|
|
153
|
+
interface PageBuilderExtension {
|
|
154
|
+
/** Namespaced, stable extension identifier, such as `besttrack.tracking`. */
|
|
155
|
+
name: string;
|
|
156
|
+
version: string;
|
|
157
|
+
order?: number;
|
|
158
|
+
dependsOn?: string[];
|
|
159
|
+
blocks?: BlockDefinition[];
|
|
160
|
+
fields?: FieldDefinition[];
|
|
161
|
+
actions?: EditorAction[];
|
|
162
|
+
renderers?: RendererDefinition[];
|
|
163
|
+
dataSources?: DataSourceDefinition[];
|
|
164
|
+
templates?: TemplateDefinition[];
|
|
165
|
+
hooks?: LifecycleHooks;
|
|
166
|
+
slots?: UISlotContribution[];
|
|
167
|
+
}
|
|
168
|
+
type ExtensionRegistryOptions = {
|
|
169
|
+
disabled?: string[];
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
type ExtensionRegistryErrorCode = "duplicate-extension" | "duplicate-definition" | "invalid-identifier" | "invalid-target" | "invalid-block-policy" | "missing-dependency" | "missing-template-dependency" | "dependency-cycle";
|
|
173
|
+
declare class ExtensionRegistryError extends Error {
|
|
174
|
+
readonly code: ExtensionRegistryErrorCode;
|
|
175
|
+
constructor(code: ExtensionRegistryErrorCode, message: string);
|
|
176
|
+
}
|
|
177
|
+
type Registered<T> = T & {
|
|
178
|
+
extension: string;
|
|
179
|
+
};
|
|
180
|
+
/** Immutable template view with explicit source and block dependency validation. */
|
|
181
|
+
declare class TemplateRegistry {
|
|
182
|
+
private readonly templateMap;
|
|
183
|
+
private constructor();
|
|
184
|
+
get templates(): readonly Registered<TemplateDefinition>[];
|
|
185
|
+
get(id: string): Registered<TemplateDefinition> | undefined;
|
|
186
|
+
static create(templates: readonly Registered<TemplateDefinition>[], blocks: readonly BlockDefinition[]): TemplateRegistry;
|
|
187
|
+
}
|
|
188
|
+
/** Immutable compiled view of all enabled extensions. */
|
|
189
|
+
declare class ExtensionRegistry {
|
|
190
|
+
readonly extensions: readonly PageBuilderExtension[];
|
|
191
|
+
private readonly blockMap;
|
|
192
|
+
private readonly fieldMap;
|
|
193
|
+
private readonly actionMap;
|
|
194
|
+
private readonly rendererMap;
|
|
195
|
+
private readonly dataSourceMap;
|
|
196
|
+
private readonly templateMap;
|
|
197
|
+
readonly templateRegistry: TemplateRegistry;
|
|
198
|
+
private readonly slotMap;
|
|
199
|
+
private readonly hooks;
|
|
200
|
+
private constructor();
|
|
201
|
+
get blocks(): readonly Registered<BlockDefinition<Record<string, unknown>>>[];
|
|
202
|
+
get fields(): readonly Registered<FieldDefinition<unknown>>[];
|
|
203
|
+
get actions(): readonly Registered<EditorAction>[];
|
|
204
|
+
get renderers(): readonly Registered<RendererDefinition>[];
|
|
205
|
+
get dataSources(): readonly Registered<DataSourceDefinition<any, unknown>>[];
|
|
206
|
+
get templates(): readonly Registered<TemplateDefinition>[];
|
|
207
|
+
getBlock(type: string): Registered<BlockDefinition<Record<string, unknown>>> | undefined;
|
|
208
|
+
getField(type: string): Registered<FieldDefinition<unknown>> | undefined;
|
|
209
|
+
getAction(id: string): Registered<EditorAction> | undefined;
|
|
210
|
+
getRenderer(id: string): Registered<RendererDefinition> | undefined;
|
|
211
|
+
getDataSource(key: string): Registered<DataSourceDefinition<any, unknown>> | undefined;
|
|
212
|
+
getTemplate(id: string): Registered<TemplateDefinition> | undefined;
|
|
213
|
+
getSlot(slot: UISlotName): readonly Registered<UISlotContribution>[];
|
|
214
|
+
notifyChange(document: PageDocument): void;
|
|
215
|
+
notifyError(error: Error): void;
|
|
216
|
+
static create(extensions: PageBuilderExtension[], options?: ExtensionRegistryOptions): ExtensionRegistry;
|
|
217
|
+
}
|
|
218
|
+
declare function createExtensionRegistry(extensions: PageBuilderExtension[], options?: ExtensionRegistryOptions): ExtensionRegistry;
|
|
219
|
+
declare function createTemplateRegistry(templates: TemplateDefinition[], blocks?: BlockDefinition[]): TemplateRegistry;
|
|
220
|
+
|
|
221
|
+
export { type BlockDefinition as B, type DataSourceDefinition as D, type EditorAction as E, type FieldConfig as F, type LifecycleHooks as L, type PageBuilderExtension as P, type RendererDefinition as R, type TemplateDefinition as T, type UISlotContribution as U, type ValidationIssue as V, type BlockEditorProps as a, type BlockOperation as b, type BlockPolicy as c, type BlockVariantDefinition as d, type EditorActionPosition as e, type EditorOperationPolicy as f, type ExtensionActionContext as g, ExtensionRegistry as h, ExtensionRegistryError as i, type ExtensionRegistryErrorCode as j, type ExtensionRegistryOptions as k, type ExtensionTarget as l, type FieldDefinition as m, type FieldProps as n, type FieldValidation as o, TemplateRegistry as p, type TemplateSource as q, type UISlotName as r, createExtensionRegistry as s, createTemplateRegistry as t };
|
package/dist/renderer.d.ts
CHANGED
package/dist/runtime.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { WebRenderer, WebRendererProps } from './renderer.js';
|
|
2
|
-
export { BlockDefinition, BlockEditorProps, BlockVariantDefinition, DataSourceDefinition, EditorAction, EditorActionPosition, ExtensionActionContext, ExtensionRegistry, ExtensionRegistryError, ExtensionRegistryErrorCode, ExtensionRegistryOptions, ExtensionTarget, FieldConfig, FieldDefinition, FieldProps, LifecycleHooks, PageBuilderExtension, RendererDefinition, TemplateDefinition, TemplateRegistry, TemplateSource, UISlotContribution, UISlotName, ValidationIssue, createExtensionRegistry, createTemplateRegistry } from './
|
|
2
|
+
export { B as BlockDefinition, a as BlockEditorProps, b as BlockOperation, c as BlockPolicy, d as BlockVariantDefinition, D as DataSourceDefinition, E as EditorAction, e as EditorActionPosition, f as EditorOperationPolicy, g as ExtensionActionContext, h as ExtensionRegistry, i as ExtensionRegistryError, j as ExtensionRegistryErrorCode, k as ExtensionRegistryOptions, l as ExtensionTarget, F as FieldConfig, m as FieldDefinition, n as FieldProps, o as FieldValidation, L as LifecycleHooks, P as PageBuilderExtension, R as RendererDefinition, T as TemplateDefinition, p as TemplateRegistry, q as TemplateSource, U as UISlotContribution, r as UISlotName, V as ValidationIssue, s as createExtensionRegistry, t as createTemplateRegistry } from './registry-eNtkEC6o.js';
|
|
3
|
+
export { maximumBlockInstances, mergeBlockPolicies, minimumBlockInstances, validateBlockPolicy, validateFieldValue, validatePageDocumentWithRegistry } from './extensions.js';
|
|
3
4
|
export { BlockNode, BlockStyleOverrides, DataBinding, JsonPrimitive, JsonValue, PageDocument, PageDocumentIssue, PageDocumentMigration, PageDocumentSchemaVersion, PageDocumentValidation, PageSettings, RenderTarget, createPageDocument, migratePageDocument, validatePageDocument } from './schema.js';
|
|
4
5
|
export { ThemeTokenName, ThemeTokenValidation, ThemeTokens, mergeThemeTokens, normalizeThemeTokens, systemThemeTokens, toThemeStyle } from './theme.js';
|
|
5
6
|
import 'react';
|
package/dist/runtime.js
CHANGED
|
@@ -3,8 +3,14 @@ import {
|
|
|
3
3
|
ExtensionRegistryError,
|
|
4
4
|
TemplateRegistry,
|
|
5
5
|
createExtensionRegistry,
|
|
6
|
-
createTemplateRegistry
|
|
7
|
-
|
|
6
|
+
createTemplateRegistry,
|
|
7
|
+
maximumBlockInstances,
|
|
8
|
+
mergeBlockPolicies,
|
|
9
|
+
minimumBlockInstances,
|
|
10
|
+
validateBlockPolicy,
|
|
11
|
+
validateFieldValue,
|
|
12
|
+
validatePageDocumentWithRegistry
|
|
13
|
+
} from "./chunk-PFCWXLWN.js";
|
|
8
14
|
import {
|
|
9
15
|
WebRenderer
|
|
10
16
|
} from "./chunk-KYXIXIWK.js";
|
|
@@ -27,10 +33,16 @@ export {
|
|
|
27
33
|
createExtensionRegistry,
|
|
28
34
|
createPageDocument,
|
|
29
35
|
createTemplateRegistry,
|
|
36
|
+
maximumBlockInstances,
|
|
37
|
+
mergeBlockPolicies,
|
|
30
38
|
mergeThemeTokens,
|
|
31
39
|
migratePageDocument,
|
|
40
|
+
minimumBlockInstances,
|
|
32
41
|
normalizeThemeTokens,
|
|
33
42
|
systemThemeTokens,
|
|
34
43
|
toThemeStyle,
|
|
35
|
-
|
|
44
|
+
validateBlockPolicy,
|
|
45
|
+
validateFieldValue,
|
|
46
|
+
validatePageDocument,
|
|
47
|
+
validatePageDocumentWithRegistry
|
|
36
48
|
};
|
package/package.json
CHANGED
package/src/styles.css
CHANGED
|
@@ -6,8 +6,15 @@ body { margin: 0; min-width: 320px; }
|
|
|
6
6
|
button:focus-visible, [role="button"]:focus-visible { outline: 2px solid var(--builder-accent); outline-offset: 2px; }
|
|
7
7
|
.pb-shell { min-height: 100vh; color: var(--builder-text); background: var(--builder-canvas-bg); }
|
|
8
8
|
.pb-header { position: sticky; top: 0; z-index: 20; min-height: 56px; padding: var(--builder-space-2) var(--builder-space-4); border-bottom: 1px solid var(--builder-border); background: var(--builder-surface); }
|
|
9
|
+
.pb-header-content { display: grid; grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); align-items: center; gap: var(--builder-space-3); }
|
|
10
|
+
.pb-header-title-group { display: flex; align-items: flex-start; gap: var(--builder-space-2); min-width: 0; }
|
|
9
11
|
.pb-page-title { display: grid; gap: 0; min-width: 0; }
|
|
10
12
|
.pb-page-title p { margin: 0; }
|
|
13
|
+
.pb-header-device-toolbar { display: flex; align-items: center; justify-self: center; gap: var(--builder-space-2); }
|
|
14
|
+
.pb-header-actions { justify-self: end; min-width: 0; }
|
|
15
|
+
.pb-zoom-control { width: 112px; }
|
|
16
|
+
.pb-page-status-card { display: grid; gap: 2px; margin-top: var(--builder-space-1); }
|
|
17
|
+
.pb-page-status-card p { margin: 0; }
|
|
11
18
|
.pb-workspace { display: grid; grid-template-columns: 64px minmax(260px, 300px) minmax(640px, 1fr) minmax(300px, 320px); min-height: calc(100vh - 56px); }
|
|
12
19
|
.pb-tool-rail { display: flex; flex-direction: column; align-items: center; gap: var(--builder-space-2); padding: var(--builder-space-2); border-right: 1px solid var(--builder-border); background: var(--builder-surface-subtle); }
|
|
13
20
|
.pb-tool-rail .Polaris-Button { min-width: 44px; min-height: 44px; }
|
|
@@ -37,6 +44,10 @@ button:focus-visible, [role="button"]:focus-visible { outline: 2px solid var(--b
|
|
|
37
44
|
.pb-inspector-field input:focus, .pb-inspector-field textarea:focus { border-color: #2563eb; box-shadow: 0 0 0 3px rgb(37 99 235 / 12%); }
|
|
38
45
|
.pb-inspector-field input:disabled, .pb-inspector-field textarea:disabled { color: #94a3b8; background: #f8fafc; cursor: not-allowed; }
|
|
39
46
|
.pb-inspector__hint { margin: 0; padding: 0 var(--builder-space-2); color: #64748b; font-size: 12px; line-height: 1.5; }
|
|
47
|
+
.pb-inspector-actions { padding-top: var(--builder-space-2); border-top: 1px solid #e2e8f0; }
|
|
48
|
+
.pb-delete-confirmation-backdrop { position: fixed; z-index: 1000; inset: 0; display: grid; place-items: center; padding: var(--builder-space-4); background: rgb(15 23 42 / 42%); }
|
|
49
|
+
.pb-delete-confirmation { display: grid; gap: var(--builder-space-4); width: min(100%, 420px); padding: var(--builder-space-5); border-radius: 12px; color: #0f172a; background: #fff; box-shadow: 0 20px 50px rgb(15 23 42 / 25%); }
|
|
50
|
+
.pb-delete-confirmation h2, .pb-delete-confirmation p { margin: 0; }
|
|
40
51
|
.pb-panel--closed { display: none; }
|
|
41
52
|
.pb-block-list { display: flex; flex: 1; flex-direction: column; gap: var(--builder-space-2); min-height: 0; }
|
|
42
53
|
.pb-block-row { display: flex; align-items: center; gap: var(--builder-space-2); padding: var(--builder-space-2); border: 1px solid transparent; border-radius: var(--builder-panel-radius); background: transparent; transition: background-color 120ms ease, border-color 120ms ease; }
|
|
@@ -111,4 +122,4 @@ button:focus-visible, [role="button"]:focus-visible { outline: 2px solid var(--b
|
|
|
111
122
|
.bt-storefront__form button { padding: 13px 18px; border: 0; border-radius: 8px; color: white; background: #203e54; font: inherit; font-weight: 600; }
|
|
112
123
|
@media (max-width: 1180px) { .pb-workspace { grid-template-columns: 64px minmax(260px, 300px) minmax(0, 1fr); } .pb-right-panel { grid-column: 2 / -1; border-top: 1px solid var(--builder-border); border-left: 0; } }
|
|
113
124
|
@media (max-width: 1023px) { .pb-header { position: static; overflow: auto; } .pb-workspace { grid-template-columns: 64px minmax(0, 1fr); } .pb-left-panel, .pb-right-panel { grid-column: 2; border-left: 0; border-right: 0; border-bottom: 1px solid var(--builder-border); } .pb-canvas-area { grid-column: 2; } }
|
|
114
|
-
@media (max-width: 640px) { .pb-header, .pb-canvas-area { padding: var(--builder-space-2); } .pb-header
|
|
125
|
+
@media (max-width: 640px) { .pb-header, .pb-canvas-area { padding: var(--builder-space-2); } .pb-header-content { min-width: 680px; } .pb-canvas-stage { padding: var(--builder-space-3) 0; } .pb-canvas-frame--tablet, .pb-canvas-frame--mobile { width: 100%; } .bt-storefront { padding: 28px 20px; } .bt-storefront__form { flex-direction: column; } }
|