@appsemble/lang-sdk 0.40.2 → 0.40.3-test.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/gridUtils.d.ts +56 -11
- package/gridUtils.js +71 -13
- package/icons.d.ts +3 -2
- package/icons.js +12 -2
- package/package.json +2 -2
- package/reference-schemas/actions/miscellaneous.js +2 -0
- package/schemas/AppDefinition.js +1 -1
- package/schemas/AppLayoutDefinition.js +8 -0
- package/schemas/BasePageDefinition.js +2 -2
- package/schemas/BlockDefinition.js +1 -4
- package/schemas/BuiltinPagesLayoutDefinition.d.ts +1 -0
- package/schemas/BuiltinPagesLayoutDefinition.js +26 -0
- package/schemas/IconRegistryEntryDefinition.js +9 -0
- package/schemas/PageLayoutDefinition.js +7 -3
- package/schemas/actions/ActionDefinition.js +1 -0
- package/schemas/actions/ScrollActionDefinition.d.ts +1 -0
- package/schemas/actions/ScrollActionDefinition.js +18 -0
- package/schemas/index.d.ts +2 -0
- package/schemas/index.js +2 -0
- package/types/action.d.ts +1 -1
- package/types/actionDefinition.d.ts +8 -2
- package/types/core.d.ts +18 -2
- package/types/gridLayout.d.ts +1 -0
- package/validation.js +153 -65
package/gridUtils.d.ts
CHANGED
|
@@ -1,8 +1,16 @@
|
|
|
1
|
-
import { type GridLayoutDefinition, type PageDefinition, type PageLayoutDefinition, type ResponsiveGridLayoutDefinition } from './types/index.js';
|
|
1
|
+
import { type BuiltinPagesLayoutDefinition, type GridLayoutDefinition, type PageDefinition, type PageLayoutDefinition, type ResponsiveGridLayoutDefinition } from './types/index.js';
|
|
2
2
|
/**
|
|
3
3
|
* The template area a page grid layout uses to place the breadcrumb trail.
|
|
4
4
|
*/
|
|
5
5
|
export declare const breadcrumbsGridArea = "breadcrumbs";
|
|
6
|
+
/**
|
|
7
|
+
* The template area a grid layout uses to place the banner that asks to verify the email address.
|
|
8
|
+
*/
|
|
9
|
+
export declare const resendBannerGridArea = "resend-banner";
|
|
10
|
+
/**
|
|
11
|
+
* The template area a grid layout uses to place the bottom navigation.
|
|
12
|
+
*/
|
|
13
|
+
export declare const bottomNavigationGridArea = "bottom-navigation";
|
|
6
14
|
/**
|
|
7
15
|
* The devices a responsive grid layout is defined for, from the smallest breakpoint up.
|
|
8
16
|
*/
|
|
@@ -15,6 +23,18 @@ export type GridDeviceName = (typeof gridDeviceOrder)[number];
|
|
|
15
23
|
* The grid a device falls back to when neither it nor a smaller device defines one.
|
|
16
24
|
*/
|
|
17
25
|
export declare const defaultGridLayout: Readonly<GridLayoutDefinition>;
|
|
26
|
+
/**
|
|
27
|
+
* The template area a built-in page grid layout uses to place the heading of the page.
|
|
28
|
+
*/
|
|
29
|
+
export declare const builtinPagesTitleGridArea = "title";
|
|
30
|
+
/**
|
|
31
|
+
* The template area a built-in page grid layout uses to place the content of the page.
|
|
32
|
+
*/
|
|
33
|
+
export declare const builtinPagesContentGridArea = "content";
|
|
34
|
+
/**
|
|
35
|
+
* The template areas a built-in page grid layout may name, in DOM order.
|
|
36
|
+
*/
|
|
37
|
+
export declare const builtinPagesGridAreaOrder: readonly ["resend-banner", "title", "content", "bottom-navigation"];
|
|
18
38
|
/**
|
|
19
39
|
* Collect the template areas a responsive grid layout defines.
|
|
20
40
|
*
|
|
@@ -37,27 +57,52 @@ export declare function getPageGridLayouts(page: PageDefinition): (PageLayoutDef
|
|
|
37
57
|
* Collect the template areas each device renders.
|
|
38
58
|
*
|
|
39
59
|
* A device that defines no template of its own inherits the one of the next smaller device, the way
|
|
40
|
-
* the app applies the grid of a page, down to
|
|
41
|
-
* device renders are not the areas its own template names.
|
|
60
|
+
* the app applies the grid of a page, down to the given default layout: `defaultGridLayout` unless
|
|
61
|
+
* the caller passes its own. So the areas a device renders are not the areas its own template names.
|
|
42
62
|
*
|
|
43
63
|
* @param layout The responsive grid layout to read.
|
|
64
|
+
* @param defaultLayout The grid a device falls back to below the smallest defined template.
|
|
44
65
|
* @returns The areas rendered by each device, keyed by device name.
|
|
45
66
|
*/
|
|
46
|
-
export declare function getCascadedGridTemplateAreas(layout: ResponsiveGridLayoutDefinition | undefined): Record<GridDeviceName, Set<string>>;
|
|
67
|
+
export declare function getCascadedGridTemplateAreas(layout: ResponsiveGridLayoutDefinition | undefined, defaultLayout?: Readonly<GridLayoutDefinition>): Record<GridDeviceName, Set<string>>;
|
|
68
|
+
/**
|
|
69
|
+
* Check whether a built-in page grid layout places an area.
|
|
70
|
+
*
|
|
71
|
+
* An optional area is named by the template of every device or by the template of none, so any
|
|
72
|
+
* device that names it opts every breakpoint in.
|
|
73
|
+
*
|
|
74
|
+
* @param layout The built-in page grid layout to check.
|
|
75
|
+
* @param area The template area to look for.
|
|
76
|
+
* @returns Whether the layout names the template area.
|
|
77
|
+
*/
|
|
78
|
+
export declare function hasBuiltinPagesGridArea(layout: BuiltinPagesLayoutDefinition | undefined, area: string): boolean;
|
|
79
|
+
/**
|
|
80
|
+
* Build the grid built-in pages fall back to below the smallest defined template.
|
|
81
|
+
*
|
|
82
|
+
* `defaultGridLayout` names the `main` area, which no built-in page renders. Built-in pages stack
|
|
83
|
+
* the areas their templates name instead, in DOM order, so an app can define the desktop grid alone
|
|
84
|
+
* without an invalid mobile fallback or an accidental heading.
|
|
85
|
+
*
|
|
86
|
+
* @param layout The built-in page grid layout to read the areas of.
|
|
87
|
+
* @returns A single column grid with one row per area the layout names.
|
|
88
|
+
*/
|
|
89
|
+
export declare function getBuiltinPagesDefaultGridLayout(layout: BuiltinPagesLayoutDefinition | undefined): GridLayoutDefinition;
|
|
47
90
|
/**
|
|
48
|
-
* Check whether a grid layout places
|
|
91
|
+
* Check whether a grid layout places an area Appsemble renders itself.
|
|
49
92
|
*
|
|
50
|
-
* Every device has to render the area, since a device that does not would place the
|
|
93
|
+
* Every device has to render the area, since a device that does not would place the element against
|
|
51
94
|
* implicit grid lines rather than inside the layout.
|
|
52
95
|
*
|
|
53
96
|
* @param layout The responsive grid layout to check.
|
|
54
|
-
* @
|
|
97
|
+
* @param area The template area to look for.
|
|
98
|
+
* @returns Whether every device renders the template area.
|
|
55
99
|
*/
|
|
56
|
-
export declare function
|
|
100
|
+
export declare function hasGridArea(layout: PageLayoutDefinition | undefined, area: string): boolean;
|
|
57
101
|
/**
|
|
58
|
-
* Check whether a page renders
|
|
102
|
+
* Check whether a page renders an area Appsemble renders itself from a grid layout.
|
|
59
103
|
*
|
|
60
104
|
* @param page The page to check the grid layouts of.
|
|
61
|
-
* @
|
|
105
|
+
* @param area The template area to look for.
|
|
106
|
+
* @returns Whether any grid layout of the page places the area.
|
|
62
107
|
*/
|
|
63
|
-
export declare function
|
|
108
|
+
export declare function pageHasGridArea(page: PageDefinition, area: string): boolean;
|
package/gridUtils.js
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
* The template area a page grid layout uses to place the breadcrumb trail.
|
|
3
3
|
*/
|
|
4
4
|
export const breadcrumbsGridArea = 'breadcrumbs';
|
|
5
|
+
/**
|
|
6
|
+
* The template area a grid layout uses to place the banner that asks to verify the email address.
|
|
7
|
+
*/
|
|
8
|
+
export const resendBannerGridArea = 'resend-banner';
|
|
9
|
+
/**
|
|
10
|
+
* The template area a grid layout uses to place the bottom navigation.
|
|
11
|
+
*/
|
|
12
|
+
export const bottomNavigationGridArea = 'bottom-navigation';
|
|
5
13
|
/**
|
|
6
14
|
* The devices a responsive grid layout is defined for, from the smallest breakpoint up.
|
|
7
15
|
*/
|
|
@@ -13,6 +21,23 @@ export const defaultGridLayout = {
|
|
|
13
21
|
columns: 1,
|
|
14
22
|
template: ['main'],
|
|
15
23
|
};
|
|
24
|
+
/**
|
|
25
|
+
* The template area a built-in page grid layout uses to place the heading of the page.
|
|
26
|
+
*/
|
|
27
|
+
export const builtinPagesTitleGridArea = 'title';
|
|
28
|
+
/**
|
|
29
|
+
* The template area a built-in page grid layout uses to place the content of the page.
|
|
30
|
+
*/
|
|
31
|
+
export const builtinPagesContentGridArea = 'content';
|
|
32
|
+
/**
|
|
33
|
+
* The template areas a built-in page grid layout may name, in DOM order.
|
|
34
|
+
*/
|
|
35
|
+
export const builtinPagesGridAreaOrder = [
|
|
36
|
+
resendBannerGridArea,
|
|
37
|
+
builtinPagesTitleGridArea,
|
|
38
|
+
builtinPagesContentGridArea,
|
|
39
|
+
bottomNavigationGridArea,
|
|
40
|
+
];
|
|
16
41
|
function getTemplateAreas(template) {
|
|
17
42
|
const areas = new Set();
|
|
18
43
|
for (const row of template) {
|
|
@@ -74,41 +99,74 @@ export function getPageGridLayouts(page) {
|
|
|
74
99
|
* Collect the template areas each device renders.
|
|
75
100
|
*
|
|
76
101
|
* A device that defines no template of its own inherits the one of the next smaller device, the way
|
|
77
|
-
* the app applies the grid of a page, down to
|
|
78
|
-
* device renders are not the areas its own template names.
|
|
102
|
+
* the app applies the grid of a page, down to the given default layout: `defaultGridLayout` unless
|
|
103
|
+
* the caller passes its own. So the areas a device renders are not the areas its own template names.
|
|
79
104
|
*
|
|
80
105
|
* @param layout The responsive grid layout to read.
|
|
106
|
+
* @param defaultLayout The grid a device falls back to below the smallest defined template.
|
|
81
107
|
* @returns The areas rendered by each device, keyed by device name.
|
|
82
108
|
*/
|
|
83
|
-
export function getCascadedGridTemplateAreas(layout) {
|
|
84
|
-
let { template } =
|
|
109
|
+
export function getCascadedGridTemplateAreas(layout, defaultLayout = defaultGridLayout) {
|
|
110
|
+
let { template } = defaultLayout;
|
|
85
111
|
return Object.fromEntries(gridDeviceOrder.map((device) => {
|
|
86
112
|
template = layout?.[device]?.layout?.template ?? template;
|
|
87
113
|
return [device, getTemplateAreas(template)];
|
|
88
114
|
}));
|
|
89
115
|
}
|
|
90
116
|
/**
|
|
91
|
-
* Check whether a grid layout places
|
|
117
|
+
* Check whether a built-in page grid layout places an area.
|
|
118
|
+
*
|
|
119
|
+
* An optional area is named by the template of every device or by the template of none, so any
|
|
120
|
+
* device that names it opts every breakpoint in.
|
|
121
|
+
*
|
|
122
|
+
* @param layout The built-in page grid layout to check.
|
|
123
|
+
* @param area The template area to look for.
|
|
124
|
+
* @returns Whether the layout names the template area.
|
|
125
|
+
*/
|
|
126
|
+
export function hasBuiltinPagesGridArea(layout, area) {
|
|
127
|
+
return getGridTemplateAreas(layout).has(area);
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Build the grid built-in pages fall back to below the smallest defined template.
|
|
131
|
+
*
|
|
132
|
+
* `defaultGridLayout` names the `main` area, which no built-in page renders. Built-in pages stack
|
|
133
|
+
* the areas their templates name instead, in DOM order, so an app can define the desktop grid alone
|
|
134
|
+
* without an invalid mobile fallback or an accidental heading.
|
|
135
|
+
*
|
|
136
|
+
* @param layout The built-in page grid layout to read the areas of.
|
|
137
|
+
* @returns A single column grid with one row per area the layout names.
|
|
138
|
+
*/
|
|
139
|
+
export function getBuiltinPagesDefaultGridLayout(layout) {
|
|
140
|
+
const areas = getGridTemplateAreas(layout);
|
|
141
|
+
return {
|
|
142
|
+
columns: 1,
|
|
143
|
+
template: builtinPagesGridAreaOrder.filter((area) => area === builtinPagesContentGridArea || areas.has(area)),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Check whether a grid layout places an area Appsemble renders itself.
|
|
92
148
|
*
|
|
93
|
-
* Every device has to render the area, since a device that does not would place the
|
|
149
|
+
* Every device has to render the area, since a device that does not would place the element against
|
|
94
150
|
* implicit grid lines rather than inside the layout.
|
|
95
151
|
*
|
|
96
152
|
* @param layout The responsive grid layout to check.
|
|
97
|
-
* @
|
|
153
|
+
* @param area The template area to look for.
|
|
154
|
+
* @returns Whether every device renders the template area.
|
|
98
155
|
*/
|
|
99
|
-
export function
|
|
156
|
+
export function hasGridArea(layout, area) {
|
|
100
157
|
if (!layout) {
|
|
101
158
|
return false;
|
|
102
159
|
}
|
|
103
|
-
return Object.values(getCascadedGridTemplateAreas(layout)).every((areas) => areas.has(
|
|
160
|
+
return Object.values(getCascadedGridTemplateAreas(layout)).every((areas) => areas.has(area));
|
|
104
161
|
}
|
|
105
162
|
/**
|
|
106
|
-
* Check whether a page renders
|
|
163
|
+
* Check whether a page renders an area Appsemble renders itself from a grid layout.
|
|
107
164
|
*
|
|
108
165
|
* @param page The page to check the grid layouts of.
|
|
109
|
-
* @
|
|
166
|
+
* @param area The template area to look for.
|
|
167
|
+
* @returns Whether any grid layout of the page places the area.
|
|
110
168
|
*/
|
|
111
|
-
export function
|
|
112
|
-
return getPageGridLayouts(page).some(
|
|
169
|
+
export function pageHasGridArea(page, area) {
|
|
170
|
+
return getPageGridLayouts(page).some((layout) => hasGridArea(layout, area));
|
|
113
171
|
}
|
|
114
172
|
//# sourceMappingURL=gridUtils.js.map
|
package/icons.d.ts
CHANGED
|
@@ -54,8 +54,9 @@ export type ResolvedIcon = {
|
|
|
54
54
|
/**
|
|
55
55
|
* Resolve an icon reference against an icon registry.
|
|
56
56
|
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
57
|
+
* A bare name listed in an entry’s `overrides` resolves like `icon:<key>` for that entry. Registry
|
|
58
|
+
* keys and entries are validated here as well, so the result is safe to render even if the
|
|
59
|
+
* registry wasn’t validated when it was published. Only own properties of the registry count.
|
|
59
60
|
*
|
|
60
61
|
* @param reference The icon reference to resolve.
|
|
61
62
|
* @param registry The app’s icon registry.
|
package/icons.js
CHANGED
|
@@ -61,8 +61,9 @@ export function isValidIconReference(reference) {
|
|
|
61
61
|
/**
|
|
62
62
|
* Resolve an icon reference against an icon registry.
|
|
63
63
|
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
64
|
+
* A bare name listed in an entry’s `overrides` resolves like `icon:<key>` for that entry. Registry
|
|
65
|
+
* keys and entries are validated here as well, so the result is safe to render even if the
|
|
66
|
+
* registry wasn’t validated when it was published. Only own properties of the registry count.
|
|
66
67
|
*
|
|
67
68
|
* @param reference The icon reference to resolve.
|
|
68
69
|
* @param registry The app’s icon registry.
|
|
@@ -70,6 +71,15 @@ export function isValidIconReference(reference) {
|
|
|
70
71
|
*/
|
|
71
72
|
export function resolveIconReference(reference, registry) {
|
|
72
73
|
const parsed = parseIconReference(reference);
|
|
74
|
+
if (parsed.type === 'fontawesome' && typeof registry === 'object' && registry) {
|
|
75
|
+
const override = Object.keys(registry).find((key) => {
|
|
76
|
+
const overrides = registry[key]?.overrides;
|
|
77
|
+
return Array.isArray(overrides) && overrides.includes(parsed.name);
|
|
78
|
+
});
|
|
79
|
+
if (override !== undefined) {
|
|
80
|
+
return resolveIconReference(`${customIconPrefix}${override}`, registry);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
73
83
|
if (parsed.type !== 'custom') {
|
|
74
84
|
return parsed;
|
|
75
85
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@appsemble/lang-sdk",
|
|
3
|
-
"version": "0.40.
|
|
3
|
+
"version": "0.40.3-test.3",
|
|
4
4
|
"description": "Language SDK for Appsemble",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"app",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"language-tags": "^1.0.0",
|
|
52
52
|
"lcm": "^0.0.4",
|
|
53
53
|
"openapi-types": "^12.0.0",
|
|
54
|
-
"parse-duration": "^
|
|
54
|
+
"parse-duration": "^2.0.0",
|
|
55
55
|
"type-fest": "^4.0.0",
|
|
56
56
|
"yaml": "^2.0.0"
|
|
57
57
|
},
|
|
@@ -15,6 +15,7 @@ import { MessageActionDefinition } from '../../schemas/actions/MessageActionDefi
|
|
|
15
15
|
import { NoopActionDefinition } from '../../schemas/actions/NoopActionDefinition.js';
|
|
16
16
|
import { NotifyActionDefinition } from '../../schemas/actions/NotifyActionDefinition.js';
|
|
17
17
|
import { RequestActionDefinition } from '../../schemas/actions/RequestActionDefinition.js';
|
|
18
|
+
import { ScrollActionDefinition } from '../../schemas/actions/ScrollActionDefinition.js';
|
|
18
19
|
import { ShareActionDefinition } from '../../schemas/actions/ShareActionDefinition.js';
|
|
19
20
|
import { StaticActionDefinition } from '../../schemas/actions/StaticActionDefinition.js';
|
|
20
21
|
import { ThrowActionDefinition } from '../../schemas/actions/ThrowActionDefinition.js';
|
|
@@ -37,6 +38,7 @@ export const miscellaneousActions = {
|
|
|
37
38
|
noop: NoopActionDefinition,
|
|
38
39
|
notify: NotifyActionDefinition,
|
|
39
40
|
request: RequestActionDefinition,
|
|
41
|
+
scroll: ScrollActionDefinition,
|
|
40
42
|
share: ShareActionDefinition,
|
|
41
43
|
static: StaticActionDefinition,
|
|
42
44
|
throw: ThrowActionDefinition,
|
package/schemas/AppDefinition.js
CHANGED
|
@@ -97,7 +97,7 @@ The most basic resource has a \`schema\` property and defines the minimal securi
|
|
|
97
97
|
|
|
98
98
|
Each key names an app-level SVG asset by its name. Keys and asset names must consist of lower case
|
|
99
99
|
letters, digits, and single hyphens. Keys are case-sensitive, and \`icon:<key>\` never falls back
|
|
100
|
-
to a Font Awesome icon.
|
|
100
|
+
to a Font Awesome icon. Use \`overrides\` to replace Font Awesome icons with a custom icon.
|
|
101
101
|
`,
|
|
102
102
|
additionalProperties: { $ref: '#/components/schemas/IconRegistryEntryDefinition' },
|
|
103
103
|
},
|
|
@@ -118,6 +118,14 @@ Only applies when \`navigation\` is set to \`top\`.
|
|
|
118
118
|
description: `Responsive grid layout for the top navigation header.
|
|
119
119
|
|
|
120
120
|
Only applies when \`navigation\` is set to \`top\` and cannot be combined with \`stackedHeader\`.
|
|
121
|
+
`,
|
|
122
|
+
},
|
|
123
|
+
builtinPages: {
|
|
124
|
+
$ref: '#/components/schemas/BuiltinPagesLayoutDefinition',
|
|
125
|
+
description: `Responsive grid layout for the built-in pages of the app.
|
|
126
|
+
|
|
127
|
+
Applies unchanged to every built-in page and every state of it. The available template areas are
|
|
128
|
+
\`resend-banner\`, \`title\`, \`content\`, and \`bottom-navigation\`.
|
|
121
129
|
`,
|
|
122
130
|
},
|
|
123
131
|
breakpoints: {
|
|
@@ -51,8 +51,8 @@ navigation. Set to \`hidden\` to display no navigational menus at all.
|
|
|
51
51
|
format: 'icon',
|
|
52
52
|
description: `An optional icon from the Font Awesome icon set, or a custom icon reference.
|
|
53
53
|
|
|
54
|
-
This will be displayed in the navigation menu. Use \`icon:<key>\` to
|
|
55
|
-
\`icons\` registry.
|
|
54
|
+
This will be displayed in the navigation menu and the breadcrumb trail. Use \`icon:<key>\` to
|
|
55
|
+
reference a key from the app’s \`icons\` registry.
|
|
56
56
|
`,
|
|
57
57
|
},
|
|
58
58
|
parent: {
|
|
@@ -91,10 +91,7 @@ The exact meaning of the parameters depends on the block type.
|
|
|
91
91
|
events: { $ref: '#/components/schemas/EventsDefinition' },
|
|
92
92
|
gridArea: {
|
|
93
93
|
type: 'string',
|
|
94
|
-
description:
|
|
95
|
-
|
|
96
|
-
The \`breadcrumbs\` area is reserved for the breadcrumb trail of the app.
|
|
97
|
-
`,
|
|
94
|
+
description: 'Grid area defined in the page layout definition.',
|
|
98
95
|
},
|
|
99
96
|
},
|
|
100
97
|
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const BuiltinPagesLayoutDefinition: import("openapi-types").OpenAPIV3.SchemaObject;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { ResponsiveGridLayoutDefinition } from './ResponsiveGridLayoutDefinition.js';
|
|
2
|
+
import { extendJSONSchema } from './utils/extendJSONSchema.js';
|
|
3
|
+
export const BuiltinPagesLayoutDefinition = extendJSONSchema(ResponsiveGridLayoutDefinition, {
|
|
4
|
+
type: 'object',
|
|
5
|
+
additionalProperties: false,
|
|
6
|
+
minProperties: 1,
|
|
7
|
+
description: `Responsive grid layout for the built-in pages of the app.
|
|
8
|
+
|
|
9
|
+
The layout applies unchanged to every built-in page and to every state of it. Without it, no
|
|
10
|
+
built-in page becomes a grid or gains a heading.
|
|
11
|
+
|
|
12
|
+
The available template areas are \`resend-banner\`, \`title\`, \`content\`, and \`bottom-navigation\`.
|
|
13
|
+
\`content\` holds everything the page renders for its current state and has to be part of the grid
|
|
14
|
+
every breakpoint renders. The others are optional and have to be named by the grid of every
|
|
15
|
+
breakpoint or of none: \`title\` is a heading that renders the name of the page, \`resend-banner\`
|
|
16
|
+
places the banner that asks to verify the email address, and \`bottom-navigation\` places the bottom
|
|
17
|
+
navigation. Without their area the banner renders above the page and the bottom navigation is fixed
|
|
18
|
+
to the bottom of the viewport; with it, they render nothing when the app shows none. The first
|
|
19
|
+
visual occurrence of the areas has to follow that order, so that keyboard, screen reader, and visual
|
|
20
|
+
order agree.
|
|
21
|
+
|
|
22
|
+
Breakpoints before the first one the layout defines render a single column with one row per area,
|
|
23
|
+
in that same order.
|
|
24
|
+
`,
|
|
25
|
+
});
|
|
26
|
+
//# sourceMappingURL=BuiltinPagesLayoutDefinition.js.map
|
|
@@ -12,6 +12,15 @@ export const IconRegistryEntryDefinition = {
|
|
|
12
12
|
|
|
13
13
|
This is an asset name, not a URL or asset ID. The asset doesn’t need to exist when the app is
|
|
14
14
|
published, but the icon only renders once an SVG asset with this name has been uploaded.
|
|
15
|
+
`,
|
|
16
|
+
},
|
|
17
|
+
overrides: {
|
|
18
|
+
type: 'array',
|
|
19
|
+
items: { type: 'string', format: 'fontawesome' },
|
|
20
|
+
uniqueItems: true,
|
|
21
|
+
description: `Font Awesome icon names to replace with this icon everywhere in the app.
|
|
22
|
+
|
|
23
|
+
Use the name from the icon’s \`fa-<name>\` class. A name can only be listed once.
|
|
15
24
|
`,
|
|
16
25
|
},
|
|
17
26
|
},
|
|
@@ -6,9 +6,13 @@ export const PageLayoutDefinition = extendJSONSchema(ResponsiveGridLayoutDefinit
|
|
|
6
6
|
minProperties: 1,
|
|
7
7
|
description: `Responsive grid layout for a page.
|
|
8
8
|
|
|
9
|
-
The \`breadcrumbs\` template
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
The \`breadcrumbs\`, \`resend-banner\`, and \`bottom-navigation\` template areas are reserved for the
|
|
10
|
+
breadcrumb trail, the banner that asks to verify the email address, and the bottom navigation of the
|
|
11
|
+
app. Each has to be part of the grid every breakpoint renders and has to be defined by every sub
|
|
12
|
+
page of a page or by none of them. \`breadcrumbs\` requires \`layout.breadcrumbs\`. Without a reserved
|
|
13
|
+
area the element keeps its default position: the trail below the title bar, the banner above the
|
|
14
|
+
page, and the bottom navigation fixed to the bottom of the viewport. \`resend-banner\` and
|
|
15
|
+
\`bottom-navigation\` render nothing when the app shows no banner or no bottom navigation.
|
|
12
16
|
`,
|
|
13
17
|
});
|
|
14
18
|
//# sourceMappingURL=PageLayoutDefinition.js.map
|
|
@@ -66,6 +66,7 @@ export const ActionDefinition = {
|
|
|
66
66
|
{ $ref: '#/components/schemas/ResourceUpdateGroupActionDefinition' },
|
|
67
67
|
{ $ref: '#/components/schemas/ResourceUpdatePositionsActionDefinition' },
|
|
68
68
|
{ $ref: '#/components/schemas/ResourcePatchActionDefinition' },
|
|
69
|
+
{ $ref: '#/components/schemas/ScrollActionDefinition' },
|
|
69
70
|
{ $ref: '#/components/schemas/ShareActionDefinition' },
|
|
70
71
|
{ $ref: '#/components/schemas/StaticActionDefinition' },
|
|
71
72
|
{ $ref: '#/components/schemas/StorageAppendActionDefinition' },
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const ScrollActionDefinition: import("openapi-types").OpenAPIV3.SchemaObject;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { BaseActionDefinition } from './BaseActionDefinition.js';
|
|
2
|
+
import { extendJSONSchema } from '../utils/extendJSONSchema.js';
|
|
3
|
+
export const ScrollActionDefinition = extendJSONSchema(BaseActionDefinition, {
|
|
4
|
+
type: 'object',
|
|
5
|
+
additionalProperties: false,
|
|
6
|
+
required: ['type', 'to'],
|
|
7
|
+
properties: {
|
|
8
|
+
type: {
|
|
9
|
+
enum: ['scroll'],
|
|
10
|
+
description: 'Scroll the page to the top or the bottom.',
|
|
11
|
+
},
|
|
12
|
+
to: {
|
|
13
|
+
enum: ['top', 'bottom'],
|
|
14
|
+
description: 'Where on the page to scroll to.',
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
//# sourceMappingURL=ScrollActionDefinition.js.map
|
package/schemas/index.d.ts
CHANGED
|
@@ -57,6 +57,7 @@ export * from './actions/ResourceSubscriptionUnsubscribeActionDefinition.js';
|
|
|
57
57
|
export * from './actions/ResourceUpdateActionDefinition.js';
|
|
58
58
|
export * from './actions/ResourceUpdateGroupActionDefinition.js';
|
|
59
59
|
export * from './actions/ResourceUpdatePositionsActionDefinition.js';
|
|
60
|
+
export * from './actions/ScrollActionDefinition.js';
|
|
60
61
|
export * from './actions/ShareActionDefinition.js';
|
|
61
62
|
export * from './actions/StaticActionDefinition.js';
|
|
62
63
|
export * from './actions/StorageAppendActionDefinition.js';
|
|
@@ -128,6 +129,7 @@ export * from './GridBreakpointsDefinition.js';
|
|
|
128
129
|
export * from './GridLayoutDefinition.js';
|
|
129
130
|
export * from './GridSpacingDefinition.js';
|
|
130
131
|
export * from './ResponsiveGridLayoutDefinition.js';
|
|
132
|
+
export * from './BuiltinPagesLayoutDefinition.js';
|
|
131
133
|
export * from './NavbarDeviceGridLayoutDefinition.js';
|
|
132
134
|
export * from './NavbarLayoutDefinition.js';
|
|
133
135
|
export * from './PageLayoutDefinition.js';
|
package/schemas/index.js
CHANGED
|
@@ -57,6 +57,7 @@ export * from './actions/ResourceSubscriptionUnsubscribeActionDefinition.js';
|
|
|
57
57
|
export * from './actions/ResourceUpdateActionDefinition.js';
|
|
58
58
|
export * from './actions/ResourceUpdateGroupActionDefinition.js';
|
|
59
59
|
export * from './actions/ResourceUpdatePositionsActionDefinition.js';
|
|
60
|
+
export * from './actions/ScrollActionDefinition.js';
|
|
60
61
|
export * from './actions/ShareActionDefinition.js';
|
|
61
62
|
export * from './actions/StaticActionDefinition.js';
|
|
62
63
|
export * from './actions/StorageAppendActionDefinition.js';
|
|
@@ -128,6 +129,7 @@ export * from './GridBreakpointsDefinition.js';
|
|
|
128
129
|
export * from './GridLayoutDefinition.js';
|
|
129
130
|
export * from './GridSpacingDefinition.js';
|
|
130
131
|
export * from './ResponsiveGridLayoutDefinition.js';
|
|
132
|
+
export * from './BuiltinPagesLayoutDefinition.js';
|
|
131
133
|
export * from './NavbarDeviceGridLayoutDefinition.js';
|
|
132
134
|
export * from './NavbarLayoutDefinition.js';
|
|
133
135
|
export * from './PageLayoutDefinition.js';
|
package/types/action.d.ts
CHANGED
|
@@ -48,5 +48,5 @@ export type ResourcePatchAction = RequestLikeAction<'resource.patch'>;
|
|
|
48
48
|
/**
|
|
49
49
|
* An action that can be called from within a block.
|
|
50
50
|
*/
|
|
51
|
-
export type Action = BaseAction<'analytics'> | BaseAction<'app.member.current.patch'> | BaseAction<'app.member.delete'> | BaseAction<'app.member.invite'> | BaseAction<'app.member.login'> | BaseAction<'app.member.logout'> | BaseAction<'app.member.properties.patch'> | BaseAction<'app.member.query'> | BaseAction<'app.member.register'> | BaseAction<'app.member.role.update'> | BaseAction<'condition'> | BaseAction<'controller'> | BaseAction<'csv.parse'> | BaseAction<'dialog.error'> | BaseAction<'dialog.ok'> | BaseAction<'dialog'> | BaseAction<'download'> | BaseAction<'each'> | BaseAction<'email'> | BaseAction<'event'> | BaseAction<'flow.back'> | BaseAction<'flow.cancel'> | BaseAction<'flow.finish'> | BaseAction<'flow.next'> | BaseAction<'flow.to'> | BaseAction<'group.member.create'> | BaseAction<'group.member.delete'> | BaseAction<'group.member.invite'> | BaseAction<'group.member.query'> | BaseAction<'group.member.role.update'> | BaseAction<'group.query'> | BaseAction<'group.selected.update'> | BaseAction<'link.back'> | BaseAction<'link.next'> | BaseAction<'match'> | BaseAction<'message'> | BaseAction<'noop'> | BaseAction<'notify'> | BaseAction<'resource.subscription.status'> | BaseAction<'resource.subscription.subscribe'> | BaseAction<'resource.subscription.toggle'> | BaseAction<'resource.subscription.unsubscribe'> | BaseAction<'share'> | BaseAction<'static'> | BaseAction<'storage.append'> | BaseAction<'storage.delete'> | BaseAction<'storage.read'> | BaseAction<'storage.subtract'> | BaseAction<'storage.update'> | BaseAction<'storage.write'> | BaseAction<'throw'> | BaseAction<'webhook'> | LinkAction | LogAction | RequestAction | ResourceCountAction | ResourceCreateAction | ResourceDeleteAction | ResourceDeleteAllAction | ResourceDeleteBulkAction | ResourceGetAction | ResourceHistoryGetAction | ResourcePatchAction | ResourceQueryAction | ResourceUpdateAction | ResourceUpdateGroupAction | ResourceUpdatePositionsAction;
|
|
51
|
+
export type Action = BaseAction<'analytics'> | BaseAction<'app.member.current.patch'> | BaseAction<'app.member.delete'> | BaseAction<'app.member.invite'> | BaseAction<'app.member.login'> | BaseAction<'app.member.logout'> | BaseAction<'app.member.properties.patch'> | BaseAction<'app.member.query'> | BaseAction<'app.member.register'> | BaseAction<'app.member.role.update'> | BaseAction<'condition'> | BaseAction<'controller'> | BaseAction<'csv.parse'> | BaseAction<'dialog.error'> | BaseAction<'dialog.ok'> | BaseAction<'dialog'> | BaseAction<'download'> | BaseAction<'each'> | BaseAction<'email'> | BaseAction<'event'> | BaseAction<'flow.back'> | BaseAction<'flow.cancel'> | BaseAction<'flow.finish'> | BaseAction<'flow.next'> | BaseAction<'flow.to'> | BaseAction<'group.member.create'> | BaseAction<'group.member.delete'> | BaseAction<'group.member.invite'> | BaseAction<'group.member.query'> | BaseAction<'group.member.role.update'> | BaseAction<'group.query'> | BaseAction<'group.selected.update'> | BaseAction<'link.back'> | BaseAction<'link.next'> | BaseAction<'match'> | BaseAction<'message'> | BaseAction<'noop'> | BaseAction<'notify'> | BaseAction<'resource.subscription.status'> | BaseAction<'resource.subscription.subscribe'> | BaseAction<'resource.subscription.toggle'> | BaseAction<'resource.subscription.unsubscribe'> | BaseAction<'scroll'> | BaseAction<'share'> | BaseAction<'static'> | BaseAction<'storage.append'> | BaseAction<'storage.delete'> | BaseAction<'storage.read'> | BaseAction<'storage.subtract'> | BaseAction<'storage.update'> | BaseAction<'storage.write'> | BaseAction<'throw'> | BaseAction<'webhook'> | LinkAction | LogAction | RequestAction | ResourceCountAction | ResourceCreateAction | ResourceDeleteAction | ResourceDeleteAllAction | ResourceDeleteBulkAction | ResourceGetAction | ResourceHistoryGetAction | ResourcePatchAction | ResourceQueryAction | ResourceUpdateAction | ResourceUpdateGroupAction | ResourceUpdatePositionsAction;
|
|
52
52
|
export {};
|
|
@@ -3,8 +3,8 @@ import { type BlockDefinition } from './block.js';
|
|
|
3
3
|
import { type BulmaColor } from './bulma.js';
|
|
4
4
|
import { type HTTPMethods } from './http.js';
|
|
5
5
|
import { type Remapper } from './remapper.js';
|
|
6
|
-
export type ActionDefinition = AnalyticsAction | AppMemberCurrentPatchAction | AppMemberDeleteAction | AppMemberInviteAction | AppMemberLoginAction | AppMemberLogoutAction | AppMemberPropertiesPatchAction | AppMemberQueryAction | AppMemberRegisterAction | AppMemberRoleUpdateAction | BaseActionDefinition<'dialog.error'> | BaseActionDefinition<'dialog.ok'> | BaseActionDefinition<'flow.back'> | BaseActionDefinition<'flow.cancel'> | BaseActionDefinition<'flow.finish'> | BaseActionDefinition<'flow.next'> | BaseActionDefinition<'group.query'> | BaseActionDefinition<'link.back'> | BaseActionDefinition<'link.next'> | BaseActionDefinition<'noop'> | BaseActionDefinition<'throw'> | ConditionActionDefinition | ControllerActionDefinition | CsvParserActionDefinition | DialogActionDefinition | DownloadActionDefinition | EachActionDefinition | EmailActionDefinition | EventActionDefinition | FlowToActionDefinition | GroupMemberCreateActionDefinition | GroupMemberDeleteActionDefinition | GroupMemberInviteActionDefinition | GroupMemberQueryActionDefinition | GroupMemberRoleUpdateActionDefinition | GroupSelectedUpdateActionDefinition | LinkActionDefinition | LogActionDefinition | MatchActionDefinition | MessageActionDefinition | NotifyActionDefinition | RequestActionDefinition | ResourceCountActionDefinition | ResourceCreateActionDefinition | ResourceDeleteActionDefinition | ResourceDeleteAllActionDefinition | ResourceDeleteBulkActionDefinition | ResourceGetActionDefinition | ResourceHistoryGetActionDefinition | ResourcePatchActionDefinition | ResourceQueryActionDefinition | ResourceSubscriptionStatusActionDefinition | ResourceSubscriptionSubscribeActionDefinition | ResourceSubscriptionToggleActionDefinition | ResourceSubscriptionUnsubscribeActionDefinition | ResourceUpdateActionDefinition | ResourceUpdateGroupActionDefinition | ResourceUpdatePositionsActionDefinition | ShareActionDefinition | StaticActionDefinition | StorageAppendActionDefinition | StorageDeleteActionDefinition | StorageReadActionDefinition | StorageSubtractActionDefinition | StorageUpdateActionDefinition | StorageWriteActionDefinition | WebhookActionDefinition;
|
|
7
|
-
export type ActionName = 'analytics' | 'app.member.current.patch' | 'app.member.delete' | 'app.member.invite' | 'app.member.login' | 'app.member.logout' | 'app.member.properties.patch' | 'app.member.query' | 'app.member.register' | 'app.member.role.update' | 'condition' | 'controller' | 'csv.parse' | 'dialog.error' | 'dialog.ok' | 'dialog' | 'download' | 'each' | 'email' | 'event' | 'flow.back' | 'flow.cancel' | 'flow.finish' | 'flow.next' | 'flow.to' | 'group.member.create' | 'group.member.delete' | 'group.member.invite' | 'group.member.query' | 'group.member.role.update' | 'group.query' | 'group.selected.update' | 'link.back' | 'link.next' | 'link' | 'log' | 'match' | 'message' | 'noop' | 'notify' | 'request' | 'resource.count' | 'resource.create' | 'resource.delete.all' | 'resource.delete.bulk' | 'resource.delete' | 'resource.get' | 'resource.history.get' | 'resource.patch' | 'resource.query' | 'resource.subscription.status' | 'resource.subscription.subscribe' | 'resource.subscription.toggle' | 'resource.subscription.unsubscribe' | 'resource.update.group' | 'resource.update.positions' | 'resource.update' | 'share' | 'static' | 'storage.append' | 'storage.delete' | 'storage.read' | 'storage.subtract' | 'storage.update' | 'storage.write' | 'throw' | 'webhook';
|
|
6
|
+
export type ActionDefinition = AnalyticsAction | AppMemberCurrentPatchAction | AppMemberDeleteAction | AppMemberInviteAction | AppMemberLoginAction | AppMemberLogoutAction | AppMemberPropertiesPatchAction | AppMemberQueryAction | AppMemberRegisterAction | AppMemberRoleUpdateAction | BaseActionDefinition<'dialog.error'> | BaseActionDefinition<'dialog.ok'> | BaseActionDefinition<'flow.back'> | BaseActionDefinition<'flow.cancel'> | BaseActionDefinition<'flow.finish'> | BaseActionDefinition<'flow.next'> | BaseActionDefinition<'group.query'> | BaseActionDefinition<'link.back'> | BaseActionDefinition<'link.next'> | BaseActionDefinition<'noop'> | BaseActionDefinition<'throw'> | ConditionActionDefinition | ControllerActionDefinition | CsvParserActionDefinition | DialogActionDefinition | DownloadActionDefinition | EachActionDefinition | EmailActionDefinition | EventActionDefinition | FlowToActionDefinition | GroupMemberCreateActionDefinition | GroupMemberDeleteActionDefinition | GroupMemberInviteActionDefinition | GroupMemberQueryActionDefinition | GroupMemberRoleUpdateActionDefinition | GroupSelectedUpdateActionDefinition | LinkActionDefinition | LogActionDefinition | MatchActionDefinition | MessageActionDefinition | NotifyActionDefinition | RequestActionDefinition | ResourceCountActionDefinition | ResourceCreateActionDefinition | ResourceDeleteActionDefinition | ResourceDeleteAllActionDefinition | ResourceDeleteBulkActionDefinition | ResourceGetActionDefinition | ResourceHistoryGetActionDefinition | ResourcePatchActionDefinition | ResourceQueryActionDefinition | ResourceSubscriptionStatusActionDefinition | ResourceSubscriptionSubscribeActionDefinition | ResourceSubscriptionToggleActionDefinition | ResourceSubscriptionUnsubscribeActionDefinition | ResourceUpdateActionDefinition | ResourceUpdateGroupActionDefinition | ResourceUpdatePositionsActionDefinition | ScrollActionDefinition | ShareActionDefinition | StaticActionDefinition | StorageAppendActionDefinition | StorageDeleteActionDefinition | StorageReadActionDefinition | StorageSubtractActionDefinition | StorageUpdateActionDefinition | StorageWriteActionDefinition | WebhookActionDefinition;
|
|
7
|
+
export type ActionName = 'analytics' | 'app.member.current.patch' | 'app.member.delete' | 'app.member.invite' | 'app.member.login' | 'app.member.logout' | 'app.member.properties.patch' | 'app.member.query' | 'app.member.register' | 'app.member.role.update' | 'condition' | 'controller' | 'csv.parse' | 'dialog.error' | 'dialog.ok' | 'dialog' | 'download' | 'each' | 'email' | 'event' | 'flow.back' | 'flow.cancel' | 'flow.finish' | 'flow.next' | 'flow.to' | 'group.member.create' | 'group.member.delete' | 'group.member.invite' | 'group.member.query' | 'group.member.role.update' | 'group.query' | 'group.selected.update' | 'link.back' | 'link.next' | 'link' | 'log' | 'match' | 'message' | 'noop' | 'notify' | 'request' | 'resource.count' | 'resource.create' | 'resource.delete.all' | 'resource.delete.bulk' | 'resource.delete' | 'resource.get' | 'resource.history.get' | 'resource.patch' | 'resource.query' | 'resource.subscription.status' | 'resource.subscription.subscribe' | 'resource.subscription.toggle' | 'resource.subscription.unsubscribe' | 'resource.update.group' | 'resource.update.positions' | 'resource.update' | 'scroll' | 'share' | 'static' | 'storage.append' | 'storage.delete' | 'storage.read' | 'storage.subtract' | 'storage.update' | 'storage.write' | 'throw' | 'webhook';
|
|
8
8
|
export interface BaseActionDefinition<T extends ActionName> {
|
|
9
9
|
/**
|
|
10
10
|
* The type of the action.
|
|
@@ -203,6 +203,12 @@ export interface LogActionDefinition extends BaseActionDefinition<'log'> {
|
|
|
203
203
|
*/
|
|
204
204
|
level?: 'error' | 'info' | 'warn';
|
|
205
205
|
}
|
|
206
|
+
export interface ScrollActionDefinition extends BaseActionDefinition<'scroll'> {
|
|
207
|
+
/**
|
|
208
|
+
* Where on the page to scroll to.
|
|
209
|
+
*/
|
|
210
|
+
to: 'bottom' | 'top';
|
|
211
|
+
}
|
|
206
212
|
export interface ShareActionDefinition extends BaseActionDefinition<'share'> {
|
|
207
213
|
/**
|
|
208
214
|
* The URL that is being shared.
|
package/types/core.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { type ResourceDefinition } from './resource.js';
|
|
|
9
9
|
import { type AppRole, type ViewRole } from './roles.js';
|
|
10
10
|
import { type Security } from './security.js';
|
|
11
11
|
import { type Theme } from './theme.js';
|
|
12
|
-
import { type GridBreakpointsDefinition, type NavbarLayoutDefinition, type PageLayoutDefinition } from './gridLayout.js';
|
|
12
|
+
import { type BuiltinPagesLayoutDefinition, type GridBreakpointsDefinition, type NavbarLayoutDefinition, type PageLayoutDefinition } from './gridLayout.js';
|
|
13
13
|
export type SettingName = 'email' | 'languages' | 'name' | 'password' | 'phoneNumber' | 'picture';
|
|
14
14
|
export type AppContentSecurityPolicy = Record<string, string[]>;
|
|
15
15
|
/**
|
|
@@ -24,6 +24,10 @@ export interface IconRegistryEntry {
|
|
|
24
24
|
* The name of the app-level asset holding the icon’s SVG artwork.
|
|
25
25
|
*/
|
|
26
26
|
asset: string;
|
|
27
|
+
/**
|
|
28
|
+
* Font Awesome icons this entry replaces throughout the app.
|
|
29
|
+
*/
|
|
30
|
+
overrides?: IconName[];
|
|
27
31
|
}
|
|
28
32
|
/**
|
|
29
33
|
* A mapping of semantic icon keys to the assets holding their artwork.
|
|
@@ -149,6 +153,18 @@ export interface AppDefinition {
|
|
|
149
153
|
* Each rendered area must be included and their first visual occurrence must follow that order.
|
|
150
154
|
*/
|
|
151
155
|
navbar?: NavbarLayoutDefinition;
|
|
156
|
+
/**
|
|
157
|
+
* Responsive grid layout for the built-in pages of the app.
|
|
158
|
+
*
|
|
159
|
+
* Applies unchanged to every built-in page and every state of it. Without this, no built-in
|
|
160
|
+
* page becomes a grid or gains a heading.
|
|
161
|
+
*
|
|
162
|
+
* The available template areas are `resend-banner`, `title`, `content`, and
|
|
163
|
+
* `bottom-navigation`. Every rendered breakpoint places `content`, and each of the others is
|
|
164
|
+
* either placed by every breakpoint or by none. Their first visual occurrence must follow that
|
|
165
|
+
* order.
|
|
166
|
+
*/
|
|
167
|
+
builtinPages?: BuiltinPagesLayoutDefinition;
|
|
152
168
|
/**
|
|
153
169
|
* Minimum viewport widths in pixels at which the tablet and desktop grid layouts apply.
|
|
154
170
|
*
|
|
@@ -283,7 +299,7 @@ export interface BasePageDefinition {
|
|
|
283
299
|
* An optional icon from the Font Awesome icon set, or an `icon:<key>` reference to the app’s
|
|
284
300
|
* `icons` registry.
|
|
285
301
|
*
|
|
286
|
-
* This will be displayed in the navigation menu.
|
|
302
|
+
* This will be displayed in the navigation menu and the breadcrumb trail.
|
|
287
303
|
*/
|
|
288
304
|
icon?: IconReference;
|
|
289
305
|
/**
|
package/types/gridLayout.d.ts
CHANGED
|
@@ -31,3 +31,4 @@ export interface ResponsiveNavbarGridLayoutDefinition {
|
|
|
31
31
|
}
|
|
32
32
|
export type NavbarLayoutDefinition = RequireAtLeastOne<ResponsiveNavbarGridLayoutDefinition>;
|
|
33
33
|
export type PageLayoutDefinition = RequireAtLeastOne<ResponsiveGridLayoutDefinition>;
|
|
34
|
+
export type BuiltinPagesLayoutDefinition = RequireAtLeastOne<ResponsiveGridLayoutDefinition>;
|
package/validation.js
CHANGED
|
@@ -6,7 +6,7 @@ import { BlockParamInstanceValidator } from './BasicValidator.js';
|
|
|
6
6
|
import { getAppBlocks, normalizeBlockName } from './blockUtils.js';
|
|
7
7
|
import { partialNormalized } from './constants/index.js';
|
|
8
8
|
import { findPageByName } from './findPageByName.js';
|
|
9
|
-
import { breadcrumbsGridArea, getCascadedGridTemplateAreas, getGridTemplateAreas, gridDeviceOrder, } from './gridUtils.js';
|
|
9
|
+
import { bottomNavigationGridArea, breadcrumbsGridArea, builtinPagesGridAreaOrder, getBuiltinPagesDefaultGridLayout, getCascadedGridTemplateAreas, getGridTemplateAreas, gridDeviceOrder, resendBannerGridArea, } from './gridUtils.js';
|
|
10
10
|
import { isValidIconName, parseIconReference } from './icons.js';
|
|
11
11
|
import { iterAction, iterApp } from './iterApp.js';
|
|
12
12
|
import { has } from './miscellaneous.js';
|
|
@@ -510,56 +510,140 @@ function validateResponsiveGridLayoutDefinition(layoutDefinition, report, path)
|
|
|
510
510
|
]);
|
|
511
511
|
}
|
|
512
512
|
}
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
513
|
+
/**
|
|
514
|
+
* Check the areas of a single grid template against the order the runtime renders them in.
|
|
515
|
+
*
|
|
516
|
+
* Reading the template row by row and cell by cell, the first occurrence of each area has to come
|
|
517
|
+
* in the order the runtime renders them, so that keyboard, screen reader, and visual order agree.
|
|
518
|
+
*
|
|
519
|
+
* @param template The template rows to check.
|
|
520
|
+
* @param rules The areas the template may name and the messages to report with.
|
|
521
|
+
* @param report The report function to use.
|
|
522
|
+
* @param templatePath The path of the template within the app definition.
|
|
523
|
+
*/
|
|
524
|
+
function validateGridTemplateAreaOrder(template, rules, report, templatePath) {
|
|
525
|
+
const allowedAreas = new Set(['.', ...rules.areaOrder]);
|
|
526
|
+
const visualAreaOrder = [];
|
|
527
|
+
const visualAreas = new Set();
|
|
528
|
+
for (const [rowIndex, row] of template.entries()) {
|
|
529
|
+
for (const area of row.split(' ')) {
|
|
530
|
+
if (!allowedAreas.has(area)) {
|
|
531
|
+
report(row, rules.unknownAreaMessage(area), [...templatePath, rowIndex]);
|
|
532
|
+
}
|
|
533
|
+
else if (area !== '.' && !visualAreas.has(area)) {
|
|
534
|
+
visualAreas.add(area);
|
|
535
|
+
visualAreaOrder.push(area);
|
|
536
|
+
}
|
|
518
537
|
}
|
|
519
|
-
|
|
520
|
-
|
|
538
|
+
}
|
|
539
|
+
let hasMissingArea = false;
|
|
540
|
+
if (rules.required) {
|
|
541
|
+
for (const area of rules.required.areas) {
|
|
542
|
+
if (!visualAreas.has(area)) {
|
|
543
|
+
hasMissingArea = true;
|
|
544
|
+
report(template, rules.required.missingAreaMessage(area), templatePath);
|
|
545
|
+
}
|
|
521
546
|
}
|
|
522
547
|
}
|
|
548
|
+
if (hasMissingArea) {
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
const expectedAreaOrder = rules.areaOrder.filter((area) => visualAreas.has(area));
|
|
552
|
+
if (visualAreaOrder.some((area, index) => area !== expectedAreaOrder[index])) {
|
|
553
|
+
report(template, rules.orderMessage(expectedAreaOrder), templatePath);
|
|
554
|
+
}
|
|
523
555
|
}
|
|
524
|
-
function
|
|
525
|
-
if (!layout
|
|
556
|
+
function validateBuiltinPagesLayout(layout, report, path) {
|
|
557
|
+
if (!layout) {
|
|
526
558
|
return;
|
|
527
559
|
}
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
560
|
+
validateResponsiveGridLayoutDefinition(layout, report, path);
|
|
561
|
+
for (const [deviceName, deviceDefinition] of Object.entries(layout)) {
|
|
562
|
+
const deviceLayout = deviceDefinition?.layout;
|
|
563
|
+
if (!deviceLayout?.template) {
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
566
|
+
validateGridTemplateAreaOrder(deviceLayout.template, {
|
|
567
|
+
areaOrder: builtinPagesGridAreaOrder,
|
|
568
|
+
unknownAreaMessage: (area) => `unknown built-in page grid area '${area}'`,
|
|
569
|
+
orderMessage: (expectedAreaOrder) => `built-in page grid areas must follow the accessible order: ${expectedAreaOrder.join(', ')}`,
|
|
570
|
+
}, report, [...path, deviceName, 'layout', 'template']);
|
|
571
|
+
}
|
|
572
|
+
// A device without a grid of its own inherits the one of the next smaller device, so the areas a
|
|
573
|
+
// breakpoint renders are not the areas its own template names. Content renders in every state, so
|
|
574
|
+
// every breakpoint needs a slot for it, and an opted in area needs one at every breakpoint too.
|
|
575
|
+
const defaultLayout = getBuiltinPagesDefaultGridLayout(layout);
|
|
576
|
+
const cascaded = getCascadedGridTemplateAreas(layout, defaultLayout);
|
|
577
|
+
for (const area of defaultLayout.template) {
|
|
578
|
+
const missing = gridDeviceOrder.filter((device) => !cascaded[device].has(area));
|
|
579
|
+
if (missing.length) {
|
|
580
|
+
report(layout, `the '${area}' grid area is missing from the grid rendered on ${missing.join(', ')}`, path);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
function validateBlockGridAreas(blocks, layoutDefinition, report, path) {
|
|
585
|
+
const templateAreas = getGridTemplateAreas(layoutDefinition);
|
|
586
|
+
for (const [idx, block] of blocks.entries()) {
|
|
587
|
+
if (layoutDefinition && block.gridArea && !templateAreas.has(block.gridArea)) {
|
|
588
|
+
report(block.gridArea, `does not match any area defined in the layout template. Available areas: ${[...templateAreas].join(', ') || 'none'}`, [...path, idx, 'gridArea']);
|
|
534
589
|
}
|
|
535
|
-
return;
|
|
536
590
|
}
|
|
537
|
-
|
|
538
|
-
|
|
591
|
+
}
|
|
592
|
+
// A device inherits the template of the next smaller one, so an area defined only for a larger
|
|
593
|
+
// device leaves the element without a slot to sit in below that breakpoint.
|
|
594
|
+
function validateGridAreaCascade(layout, area, report, path) {
|
|
539
595
|
const cascaded = getCascadedGridTemplateAreas(layout);
|
|
540
|
-
const missing = gridDeviceOrder.filter((device) => !cascaded[device].has(
|
|
596
|
+
const missing = gridDeviceOrder.filter((device) => !cascaded[device].has(area));
|
|
541
597
|
if (missing.length) {
|
|
542
|
-
report(layout, `the '${
|
|
598
|
+
report(layout, `the '${area}' grid area is missing from the grid rendered on ${missing.join(', ')}`, path);
|
|
543
599
|
}
|
|
544
600
|
}
|
|
545
|
-
function
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
// alone whether a page renders the trail from its grid or below the title bar.
|
|
549
|
-
const declaresArea = ([subPage]) => getGridTemplateAreas(subPage.layout).has(breadcrumbsGridArea);
|
|
550
|
-
const declaring = subPages.filter(declaresArea);
|
|
551
|
-
if (!declaring.length || declaring.length === subPages.length) {
|
|
601
|
+
function validateReservedGridAreas(layout, report, path, breadcrumbsEnabled) {
|
|
602
|
+
const areas = getGridTemplateAreas(layout);
|
|
603
|
+
if (!layout || !areas.size) {
|
|
552
604
|
return;
|
|
553
605
|
}
|
|
554
|
-
|
|
555
|
-
if (
|
|
556
|
-
|
|
606
|
+
if (areas.has(breadcrumbsGridArea)) {
|
|
607
|
+
if (breadcrumbsEnabled) {
|
|
608
|
+
validateGridAreaCascade(layout, breadcrumbsGridArea, report, path);
|
|
609
|
+
}
|
|
610
|
+
else {
|
|
611
|
+
for (const [deviceName, deviceDefinition] of Object.entries(layout)) {
|
|
612
|
+
const template = deviceDefinition?.layout?.template;
|
|
613
|
+
if (template?.some((row) => row.split(' ').includes(breadcrumbsGridArea))) {
|
|
614
|
+
report(template, `the '${breadcrumbsGridArea}' grid area requires layout.breadcrumbs to be enabled`, [...path, deviceName, 'layout', 'template']);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
// The banner and the bottom navigation render nothing when the app has none, so naming their
|
|
620
|
+
// area only has to give every device a slot for it.
|
|
621
|
+
for (const area of [resendBannerGridArea, bottomNavigationGridArea]) {
|
|
622
|
+
if (areas.has(area)) {
|
|
623
|
+
validateGridAreaCascade(layout, area, report, path);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
function validateConsistentGridAreas(subPages, report) {
|
|
628
|
+
// A reserved area belongs to the page, so it may not appear on one sub page and be missing from
|
|
629
|
+
// the next. Defining the area consistently also lets the app runtime tell from the page definition
|
|
630
|
+
// alone whether a page renders the element from its grid or in its default position.
|
|
631
|
+
for (const area of [breadcrumbsGridArea, resendBannerGridArea, bottomNavigationGridArea]) {
|
|
632
|
+
const declaresArea = ([subPage]) => getGridTemplateAreas(subPage.layout).has(area);
|
|
633
|
+
const declaring = subPages.filter(declaresArea);
|
|
634
|
+
if (!declaring.length || declaring.length === subPages.length) {
|
|
635
|
+
continue;
|
|
636
|
+
}
|
|
637
|
+
for (const entry of subPages) {
|
|
638
|
+
if (!declaresArea(entry)) {
|
|
639
|
+
report(entry[0], `the '${area}' grid area must be defined by every sub page of this page or by none`, entry[1]);
|
|
640
|
+
}
|
|
557
641
|
}
|
|
558
642
|
}
|
|
559
643
|
}
|
|
560
644
|
function validateSubPageLayout(layout, blocks, report, path, breadcrumbsEnabled) {
|
|
561
645
|
validateResponsiveGridLayoutDefinition(layout, report, path);
|
|
562
|
-
|
|
646
|
+
validateReservedGridAreas(layout, report, path, breadcrumbsEnabled);
|
|
563
647
|
if (blocks) {
|
|
564
648
|
validateBlockGridAreas(blocks, layout, report, [...path, 'blocks']);
|
|
565
649
|
}
|
|
@@ -584,42 +668,23 @@ function validateGridLayout(definition, report) {
|
|
|
584
668
|
report(navbarLayout, 'cannot be combined with stackedHeader', ['layout', 'navbar']);
|
|
585
669
|
}
|
|
586
670
|
const navbarAreaOrder = ['logo', 'name', 'navigation', 'controls'];
|
|
587
|
-
const allowedAreas = new Set(['.', ...navbarAreaOrder]);
|
|
588
671
|
const requiredAreas = definition.layout?.logo?.position === 'navbar' ? navbarAreaOrder : navbarAreaOrder.slice(1);
|
|
589
672
|
for (const [deviceName, deviceDefinition] of Object.entries(navbarLayout)) {
|
|
590
673
|
if (!deviceDefinition?.layout?.template) {
|
|
591
674
|
continue;
|
|
592
675
|
}
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
else if (area !== '.' && !visualAreas.has(area)) {
|
|
603
|
-
visualAreas.add(area);
|
|
604
|
-
visualAreaOrder.push(area);
|
|
605
|
-
}
|
|
606
|
-
}
|
|
607
|
-
}
|
|
608
|
-
let hasMissingArea = false;
|
|
609
|
-
for (const area of requiredAreas) {
|
|
610
|
-
if (!visualAreas.has(area)) {
|
|
611
|
-
hasMissingArea = true;
|
|
612
|
-
report(template, `navbar grid is missing required area '${area}'`, templatePath);
|
|
613
|
-
}
|
|
614
|
-
}
|
|
615
|
-
if (!hasMissingArea) {
|
|
616
|
-
const expectedAreaOrder = navbarAreaOrder.filter((area) => visualAreas.has(area));
|
|
617
|
-
if (visualAreaOrder.some((area, index) => area !== expectedAreaOrder[index])) {
|
|
618
|
-
report(template, `navbar grid areas must follow the accessible order: ${expectedAreaOrder.join(', ')}`, templatePath);
|
|
619
|
-
}
|
|
620
|
-
}
|
|
676
|
+
validateGridTemplateAreaOrder(deviceDefinition.layout.template, {
|
|
677
|
+
areaOrder: navbarAreaOrder,
|
|
678
|
+
required: {
|
|
679
|
+
areas: requiredAreas,
|
|
680
|
+
missingAreaMessage: (area) => `navbar grid is missing required area '${area}'`,
|
|
681
|
+
},
|
|
682
|
+
unknownAreaMessage: (area) => `unknown navbar grid area '${area}'`,
|
|
683
|
+
orderMessage: (expectedAreaOrder) => `navbar grid areas must follow the accessible order: ${expectedAreaOrder.join(', ')}`,
|
|
684
|
+
}, report, ['layout', 'navbar', deviceName, 'layout', 'template']);
|
|
621
685
|
}
|
|
622
686
|
}
|
|
687
|
+
validateBuiltinPagesLayout(definition.layout?.builtinPages, report, ['layout', 'builtinPages']);
|
|
623
688
|
const breadcrumbsEnabled = definition.layout?.breadcrumbs === true;
|
|
624
689
|
iterApp(definition, {
|
|
625
690
|
onPage(page, path) {
|
|
@@ -640,7 +705,7 @@ function validateGridLayout(definition, report) {
|
|
|
640
705
|
for (const [tab, tabPath] of tabs) {
|
|
641
706
|
validateSubPageLayout(tab.layout, tab.blocks, report, tabPath, breadcrumbsEnabled);
|
|
642
707
|
}
|
|
643
|
-
|
|
708
|
+
validateConsistentGridAreas(tabs, report);
|
|
644
709
|
return;
|
|
645
710
|
}
|
|
646
711
|
// Flow page
|
|
@@ -653,7 +718,7 @@ function validateGridLayout(definition, report) {
|
|
|
653
718
|
for (const [step, stepPath] of steps) {
|
|
654
719
|
validateSubPageLayout(step.layout, step.blocks, report, stepPath, breadcrumbsEnabled);
|
|
655
720
|
}
|
|
656
|
-
|
|
721
|
+
validateConsistentGridAreas(steps, report);
|
|
657
722
|
return;
|
|
658
723
|
}
|
|
659
724
|
// Loop page
|
|
@@ -667,7 +732,7 @@ function validateGridLayout(definition, report) {
|
|
|
667
732
|
for (const [subPage, subPagePath] of subPages) {
|
|
668
733
|
validateSubPageLayout(subPage.layout, subPage.blocks, report, subPagePath, breadcrumbsEnabled);
|
|
669
734
|
}
|
|
670
|
-
|
|
735
|
+
validateConsistentGridAreas(subPages, report);
|
|
671
736
|
}
|
|
672
737
|
},
|
|
673
738
|
});
|
|
@@ -1243,13 +1308,36 @@ function validateIcons({ icons }, report) {
|
|
|
1243
1308
|
if (!icons) {
|
|
1244
1309
|
return;
|
|
1245
1310
|
}
|
|
1246
|
-
|
|
1311
|
+
// Each overridden icon name, mapped to the key of the entry listing it.
|
|
1312
|
+
const overridden = new Map();
|
|
1313
|
+
for (const [key, entry] of Object.entries(icons)) {
|
|
1247
1314
|
if (!isValidIconName(key)) {
|
|
1248
1315
|
report(key, 'is not a valid icon key; use lower case letters, digits, and single hyphens', [
|
|
1249
1316
|
'icons',
|
|
1250
1317
|
key,
|
|
1251
1318
|
]);
|
|
1252
1319
|
}
|
|
1320
|
+
// The schema reports entries and override lists of the wrong shape.
|
|
1321
|
+
if (!Array.isArray(entry?.overrides)) {
|
|
1322
|
+
continue;
|
|
1323
|
+
}
|
|
1324
|
+
for (const [index, name] of entry.overrides.entries()) {
|
|
1325
|
+
if (typeof name !== 'string') {
|
|
1326
|
+
continue;
|
|
1327
|
+
}
|
|
1328
|
+
const earlier = overridden.get(name);
|
|
1329
|
+
if (earlier === undefined) {
|
|
1330
|
+
overridden.set(name, key);
|
|
1331
|
+
}
|
|
1332
|
+
else if (earlier !== key) {
|
|
1333
|
+
report(name, `is already overridden by icons.${earlier}`, [
|
|
1334
|
+
'icons',
|
|
1335
|
+
key,
|
|
1336
|
+
'overrides',
|
|
1337
|
+
index,
|
|
1338
|
+
]);
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1253
1341
|
}
|
|
1254
1342
|
}
|
|
1255
1343
|
/**
|