@appsemble/lang-sdk 0.40.10 → 0.40.11-test.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.
@@ -18,6 +18,12 @@ export declare const partialNormalized: RegExp;
18
18
  * A pattern for exactly matching a lower case hyphen separated string.
19
19
  */
20
20
  export declare const normalized: RegExp;
21
+ /**
22
+ * A pattern for validating a theme variable (custom CSS property) value.
23
+ *
24
+ * Any single-line CSS value that cannot escape its declaration block is allowed.
25
+ */
26
+ export declare const themeVariableValue: RegExp;
21
27
  /**
22
28
  * A pattern for matching the block name pattern of @organization/project.
23
29
  */
@@ -19,6 +19,12 @@ export const partialNormalized = /([0-9a-z](?:(?!.*--)[-0-9a-z]*[0-9a-z])?)/;
19
19
  * A pattern for exactly matching a lower case hyphen separated string.
20
20
  */
21
21
  export const normalized = new RegExp(`^${partialNormalized.source}$`);
22
+ /**
23
+ * A pattern for validating a theme variable (custom CSS property) value.
24
+ *
25
+ * Any single-line CSS value that cannot escape its declaration block is allowed.
26
+ */
27
+ export const themeVariableValue = /^[^\n\r;{}]+$/;
22
28
  /**
23
29
  * A pattern for matching the block name pattern of @organization/project.
24
30
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@appsemble/lang-sdk",
3
- "version": "0.40.10",
3
+ "version": "0.40.11-test.0",
4
4
  "description": "Language SDK for Appsemble",
5
5
  "keywords": [
6
6
  "app",
@@ -1,4 +1,4 @@
1
- import { partialNormalized, semver } from '../constants/index.js';
1
+ import { normalized, partialNormalized, semver } from '../constants/index.js';
2
2
  export const BlockDefinition = {
3
3
  type: 'object',
4
4
  description: 'A block that is displayed on a page.',
@@ -29,6 +29,19 @@ Some examples:
29
29
 
30
30
  Since blocks may change over time, a version must be specified in order to ensure that the block
31
31
  will remain the same until it is manually updated.
32
+ `,
33
+ },
34
+ name: {
35
+ type: 'string',
36
+ pattern: normalized.source,
37
+ maxLength: 40,
38
+ description: `A name which identifies this single block instance.
39
+
40
+ The name is rendered as a \`data-block-name\` attribute on the wrapper element of the block, so app
41
+ styles can target one instance instead of every block of the same type. Names must be lower case and
42
+ hyphen separated.
43
+
44
+ For example, a block named \`screen-title\` is targeted with \`[data-block-name='screen-title']\`.
32
45
  `,
33
46
  },
34
47
  layout: {
package/schemas/Theme.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { baseTheme } from '../constants/baseTheme.js';
2
- import { hexColor } from '../constants/patterns.js';
2
+ import { hexColor, normalized, themeVariableValue } from '../constants/patterns.js';
3
3
  export const Theme = {
4
4
  type: 'object',
5
5
  description: 'A generic theme definition.',
@@ -90,6 +90,38 @@ the CSS of the app and its blocks. It falls back to the body font.
90
90
  { $ref: '#/components/schemas/GoogleFontDefinition' },
91
91
  ],
92
92
  },
93
+ variables: {
94
+ type: 'object',
95
+ description: `Custom CSS properties (design tokens) exposed on \`:root\` in the theme CSS.
96
+
97
+ Each entry is emitted as a \`--<name>\` custom property. Names must be lower case and hyphen
98
+ separated. Custom properties inherit into every block's shadow DOM, so tokens defined once are
99
+ usable in core, shared, and block styles.
100
+
101
+ For example:
102
+
103
+ \`\`\`yaml
104
+ theme:
105
+ variables:
106
+ night: '#0e1220'
107
+ font-display: "'Baloo 2', sans-serif"
108
+ \`\`\`
109
+
110
+ These can be referenced in custom CSS as \`var(--night)\` and \`var(--font-display)\`.
111
+ `,
112
+ minProperties: 1,
113
+ maxProperties: 32,
114
+ // The validator supports propertyNames, but the OpenAPI schema object type does not know it.
115
+ propertyNames: {
116
+ pattern: normalized.source,
117
+ maxLength: 40,
118
+ },
119
+ additionalProperties: {
120
+ type: 'string',
121
+ pattern: themeVariableValue.source,
122
+ maxLength: 128,
123
+ },
124
+ },
93
125
  },
94
126
  };
95
127
  //# sourceMappingURL=Theme.js.map
package/types/block.d.ts CHANGED
@@ -48,6 +48,14 @@ export interface BlockDefinition extends ControllerDefinition {
48
48
  * ^\d+\.\d+\.\d+$
49
49
  */
50
50
  version: string;
51
+ /**
52
+ * A name which identifies this single block instance.
53
+ *
54
+ * It is rendered as a `data-block-name` attribute on the wrapper element of the block, so app
55
+ * styles can target one instance instead of every block of the same type. Names must be lower
56
+ * case and hyphen separated.
57
+ */
58
+ name?: string;
51
59
  /**
52
60
  * An optional header to render above the block.
53
61
  */
package/types/theme.d.ts CHANGED
@@ -51,6 +51,13 @@ export interface Theme {
51
51
  * The name of a font available on Google fonts.
52
52
  */
53
53
  font: FontDefinition;
54
+ /**
55
+ * Custom CSS properties (design tokens) exposed on `:root` in the compiled theme CSS.
56
+ *
57
+ * Each entry is emitted as a `--<name>` custom property. Custom properties inherit into every
58
+ * block's shadow DOM, so tokens defined once are usable in core, shared, and block styles.
59
+ */
60
+ variables?: Record<string, string>;
54
61
  /**
55
62
  * A second font family the app loads, exposed as the `--font-family-display` custom property for
56
63
  * the CSS of the app and its blocks.
package/validation.js CHANGED
@@ -9,7 +9,7 @@ import { findPageByName } from './findPageByName.js';
9
9
  import { isAppLink } from './pageUtils.js';
10
10
  import { bottomNavigationGridArea, breadcrumbsGridArea, builtinPagesGridAreaOrder, getBuiltinPagesDefaultGridLayout, getCascadedGridTemplateAreas, getGridTemplateAreas, gridDeviceOrder, resendBannerGridArea, } from './gridUtils.js';
11
11
  import { isValidIconName, parseIconReference } from './icons.js';
12
- import { iterApp } from './iterApp.js';
12
+ import { iterApp, iterBlockList } from './iterApp.js';
13
13
  import { has } from './miscellaneous.js';
14
14
  import { normalize } from './normalize.js';
15
15
  import { serverActions } from './serverActions.js';
@@ -167,6 +167,74 @@ function validatePageParents(definition, report) {
167
167
  }
168
168
  });
169
169
  }
170
+ /**
171
+ * Validates the blocks in the app definition to ensure there are no duplicate block names.
172
+ *
173
+ * A block name identifies a single block within the screen it is rendered in. A page renders one
174
+ * of its tabs or steps at a time, so uniqueness is scoped to the block list the block belongs to.
175
+ * The same name may be used on other pages, tabs and steps, which lets an app style the same role
176
+ * everywhere with one rule.
177
+ *
178
+ * @param definition The definition of the app
179
+ * @param report A function used to report a value.
180
+ */
181
+ function validateUniqueBlockNames(definition, report) {
182
+ function checkBlockList(blocks, path) {
183
+ const names = new Set();
184
+ iterBlockList(blocks, {
185
+ onBlock(block, blockPath) {
186
+ if (block.name) {
187
+ if (names.has(block.name)) {
188
+ report(block.name, 'is a duplicate block name', [...blockPath, 'name']);
189
+ }
190
+ else {
191
+ names.add(block.name);
192
+ }
193
+ }
194
+ },
195
+ }, path);
196
+ }
197
+ iterApp(definition, {
198
+ onPage(page, path) {
199
+ // Basic page
200
+ if (page.type === 'page' || page.type === undefined) {
201
+ checkBlockList(page.blocks, [...path, 'blocks']);
202
+ return;
203
+ }
204
+ // Tabs page
205
+ if (page.type === 'tabs') {
206
+ const tabsPage = page;
207
+ if (tabsPage.tabs) {
208
+ for (const [tabId, tab] of tabsPage.tabs.entries()) {
209
+ checkBlockList(tab.blocks, [...path, 'tabs', tabId, 'blocks']);
210
+ }
211
+ }
212
+ else if (tabsPage.definition?.foreach) {
213
+ checkBlockList(tabsPage.definition.foreach.blocks, [
214
+ ...path,
215
+ 'definition',
216
+ 'foreach',
217
+ 'blocks',
218
+ ]);
219
+ }
220
+ return;
221
+ }
222
+ // Flow page
223
+ if (page.type === 'flow' && page.steps) {
224
+ const flowPage = page;
225
+ for (const [stepId, step] of flowPage.steps.entries()) {
226
+ checkBlockList(step.blocks, [...path, 'steps', stepId, 'blocks']);
227
+ }
228
+ return;
229
+ }
230
+ // Loop page
231
+ if (page.type === 'loop') {
232
+ const loopPage = page;
233
+ checkBlockList(loopPage.foreach.blocks, [...path, 'foreach', 'blocks']);
234
+ }
235
+ },
236
+ });
237
+ }
170
238
  function validateMembersSchema(definition, report) {
171
239
  if (!definition.members?.properties) {
172
240
  return;
@@ -1869,6 +1937,7 @@ export async function validateAppDefinition(definition, getBlockVersions, contro
1869
1937
  check(() => validateActions(clonedDefinition, report));
1870
1938
  check(() => validateEvents(clonedDefinition, blockVersionMap, report));
1871
1939
  check(() => validateUniquePageNames(clonedDefinition, report));
1940
+ check(() => validateUniqueBlockNames(clonedDefinition, report));
1872
1941
  check(() => validateGridLayout(clonedDefinition, report));
1873
1942
  }
1874
1943
  catch (error) {