@appsemble/lang-sdk 0.37.6 → 0.38.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/iterApp.js CHANGED
@@ -121,8 +121,13 @@ export function iterPage(page, callbacks, prefix = []) {
121
121
  if (result) {
122
122
  return true;
123
123
  }
124
- const { foreach } = page;
125
- return ['steps.first', 'steps', 'steps.last'].some((suffix) => iterBlockList(foreach?.blocks ?? [], callbacks, [...prefix, suffix, 'blocks']));
124
+ const { end, foreach, start } = page;
125
+ const subPages = [
126
+ ['steps.first', start ?? foreach],
127
+ ['steps', foreach],
128
+ ['steps.last', end ?? foreach],
129
+ ];
130
+ return subPages.some(([suffix, subPage]) => iterBlockList(subPage?.blocks ?? [], callbacks, [...prefix, suffix, 'blocks']));
126
131
  }
127
132
  if (page.type === 'container') {
128
133
  let result = false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@appsemble/lang-sdk",
3
- "version": "0.37.6",
3
+ "version": "0.38.0",
4
4
  "description": "Language SDK for Appsemble",
5
5
  "keywords": [
6
6
  "app",
@@ -58,7 +58,7 @@
58
58
  "devDependencies": {
59
59
  "@types/language-tags": "1.0.4",
60
60
  "@types/lcm": "0.0.2",
61
- "vitest": "4.1.10"
61
+ "vitest": "4.1.11"
62
62
  },
63
63
  "engines": {
64
64
  "node": ">=24"
@@ -104,6 +104,20 @@ Only applies when \`navigation\` is set to \`top\`.
104
104
  type: 'boolean',
105
105
  default: false,
106
106
  },
107
+ navbar: {
108
+ $ref: '#/components/schemas/NavbarLayoutDefinition',
109
+ description: `Responsive grid layout for the top navigation header.
110
+
111
+ Only applies when \`navigation\` is set to \`top\` and cannot be combined with \`stackedHeader\`.
112
+ `,
113
+ },
114
+ breakpoints: {
115
+ $ref: '#/components/schemas/GridBreakpointsDefinition',
116
+ description: `Minimum viewport widths in pixels at which the tablet and desktop grid layouts apply.
117
+
118
+ Applies to every responsive grid layout in the app, including pages and the navbar.
119
+ `,
120
+ },
107
121
  hideGroupDropdown: {
108
122
  description: `Whether to hide the group dropdown.
109
123
 
@@ -0,0 +1,2 @@
1
+ import { type OpenAPIV3 } from 'openapi-types';
2
+ export declare const GridBreakpointsDefinition: OpenAPIV3.NonArraySchemaObject;
@@ -0,0 +1,22 @@
1
+ export const GridBreakpointsDefinition = {
2
+ type: 'object',
3
+ description: `Minimum viewport widths in pixels at which the tablet and desktop grid layouts apply.
4
+
5
+ The mobile layout always applies from a width of 0. Each breakpoint must be smaller than the next.
6
+ `,
7
+ additionalProperties: false,
8
+ minProperties: 1,
9
+ properties: {
10
+ tablet: {
11
+ type: 'integer',
12
+ minimum: 0,
13
+ description: 'Minimum viewport width in pixels at which the tablet grid layout applies.',
14
+ },
15
+ desktop: {
16
+ type: 'integer',
17
+ minimum: 0,
18
+ description: 'Minimum viewport width in pixels at which the desktop grid layout applies.',
19
+ },
20
+ },
21
+ };
22
+ //# sourceMappingURL=GridBreakpointsDefinition.js.map
@@ -5,9 +5,9 @@ export const GridSpacingDefinition = {
5
5
  properties: {
6
6
  unit: {
7
7
  type: 'string',
8
- description: 'CSS unit for spacing calculations (e.g., "1rem", "8px")',
8
+ description: 'CSS unit or custom property for spacing calculations (e.g., "1rem", "8px", "var(--grid-unit)")',
9
9
  default: '1rem',
10
- pattern: '^-?(\\d+\\.?\\d*|\\.\\d+)(rem|em|px|%|vh|vw|vmin|vmax|ch|ex|cm|mm|in|pt|pc)$',
10
+ pattern: '^(?:-?(\\d+\\.?\\d*|\\.\\d+)(rem|em|px|%|vh|vw|vmin|vmax|ch|ex|cm|mm|in|pt|pc)|var\\(--[A-Za-z_][A-Za-z0-9_-]*\\))$',
11
11
  },
12
12
  gap: {
13
13
  type: 'number',
@@ -27,6 +27,14 @@ By default the flow page retains it's data after navigating once. Set to false t
27
27
  `,
28
28
  default: true,
29
29
  },
30
+ start: {
31
+ $ref: '#/components/schemas/SubPage',
32
+ description: 'A page to display before the generated loop pages.',
33
+ },
34
+ end: {
35
+ $ref: '#/components/schemas/SubPage',
36
+ description: 'A page to display after the generated loop pages.',
37
+ },
30
38
  },
31
39
  });
32
40
  //# sourceMappingURL=LoopPageDefinition.js.map
@@ -0,0 +1 @@
1
+ export declare const NavbarDeviceGridLayoutDefinition: import("openapi-types").OpenAPIV3.SchemaObject;
@@ -0,0 +1,9 @@
1
+ import { DeviceGridLayoutDefinition } from './DeviceGridLayoutDefinition.js';
2
+ import { extendJSONSchema } from './utils/extendJSONSchema.js';
3
+ export const NavbarDeviceGridLayoutDefinition = extendJSONSchema(DeviceGridLayoutDefinition, {
4
+ type: 'object',
5
+ additionalProperties: false,
6
+ required: ['layout'],
7
+ description: 'Grid layout definition for a navbar breakpoint',
8
+ });
9
+ //# sourceMappingURL=NavbarDeviceGridLayoutDefinition.js.map
@@ -0,0 +1 @@
1
+ export declare const NavbarLayoutDefinition: import("openapi-types").OpenAPIV3.SchemaObject;
@@ -0,0 +1,24 @@
1
+ import { ResponsiveGridLayoutDefinition } from './ResponsiveGridLayoutDefinition.js';
2
+ import { extendJSONSchema } from './utils/extendJSONSchema.js';
3
+ export const NavbarLayoutDefinition = extendJSONSchema(ResponsiveGridLayoutDefinition, {
4
+ type: 'object',
5
+ additionalProperties: false,
6
+ minProperties: 1,
7
+ description: `Responsive grid layout for the top navigation header.
8
+
9
+ The available template areas are logo, name, navigation, and controls.
10
+ Each rendered area must be included and their first visual occurrence must follow that order.
11
+ `,
12
+ properties: {
13
+ desktop: {
14
+ $ref: '#/components/schemas/NavbarDeviceGridLayoutDefinition',
15
+ },
16
+ mobile: {
17
+ $ref: '#/components/schemas/NavbarDeviceGridLayoutDefinition',
18
+ },
19
+ tablet: {
20
+ $ref: '#/components/schemas/NavbarDeviceGridLayoutDefinition',
21
+ },
22
+ },
23
+ });
24
+ //# sourceMappingURL=NavbarLayoutDefinition.js.map
@@ -1,2 +1 @@
1
- import { type OpenAPIV3 } from 'openapi-types';
2
- export declare const PageLayoutDefinition: OpenAPIV3.NonArraySchemaObject;
1
+ export declare const PageLayoutDefinition: import("openapi-types").OpenAPIV3.SchemaObject;
@@ -1,18 +1,9 @@
1
- export const PageLayoutDefinition = {
1
+ import { ResponsiveGridLayoutDefinition } from './ResponsiveGridLayoutDefinition.js';
2
+ import { extendJSONSchema } from './utils/extendJSONSchema.js';
3
+ export const PageLayoutDefinition = extendJSONSchema(ResponsiveGridLayoutDefinition, {
2
4
  type: 'object',
3
- description: 'Grid layout of the page, define as an anchor to re-use across multiple pages',
4
5
  additionalProperties: false,
5
6
  minProperties: 1,
6
- properties: {
7
- mobile: {
8
- $ref: '#/components/schemas/DeviceGridLayoutDefinition',
9
- },
10
- tablet: {
11
- $ref: '#/components/schemas/DeviceGridLayoutDefinition',
12
- },
13
- desktop: {
14
- $ref: '#/components/schemas/DeviceGridLayoutDefinition',
15
- },
16
- },
17
- };
7
+ description: 'Responsive grid layout for a page',
8
+ });
18
9
  //# sourceMappingURL=PageLayoutDefinition.js.map
@@ -100,10 +100,10 @@ name of the resource and how it should behave.
100
100
  },
101
101
  enforceOrderingGroupByFields: {
102
102
  type: 'array',
103
- pattern: '^[a-zA-Z0-9]*$',
104
103
  description: 'Group resources by fields when handling resource positions',
105
104
  items: {
106
105
  type: 'string',
106
+ pattern: '^[a-zA-Z0-9]*$',
107
107
  },
108
108
  minItems: 1,
109
109
  },
@@ -0,0 +1,2 @@
1
+ import { type OpenAPIV3 } from 'openapi-types';
2
+ export declare const ResponsiveGridLayoutDefinition: OpenAPIV3.NonArraySchemaObject;
@@ -0,0 +1,18 @@
1
+ export const ResponsiveGridLayoutDefinition = {
2
+ type: 'object',
3
+ description: 'Responsive grid layout',
4
+ additionalProperties: false,
5
+ minProperties: 1,
6
+ properties: {
7
+ mobile: {
8
+ $ref: '#/components/schemas/DeviceGridLayoutDefinition',
9
+ },
10
+ tablet: {
11
+ $ref: '#/components/schemas/DeviceGridLayoutDefinition',
12
+ },
13
+ desktop: {
14
+ $ref: '#/components/schemas/DeviceGridLayoutDefinition',
15
+ },
16
+ },
17
+ };
18
+ //# sourceMappingURL=ResponsiveGridLayoutDefinition.js.map
@@ -124,6 +124,10 @@ export * from './TabsPageDefinition.js';
124
124
  export * from './Theme.js';
125
125
  export * from './WebhookDefinition.js';
126
126
  export * from './DeviceGridLayoutDefinition.js';
127
+ export * from './GridBreakpointsDefinition.js';
127
128
  export * from './GridLayoutDefinition.js';
128
129
  export * from './GridSpacingDefinition.js';
130
+ export * from './ResponsiveGridLayoutDefinition.js';
131
+ export * from './NavbarDeviceGridLayoutDefinition.js';
132
+ export * from './NavbarLayoutDefinition.js';
129
133
  export * from './PageLayoutDefinition.js';
package/schemas/index.js CHANGED
@@ -124,7 +124,11 @@ export * from './TabsPageDefinition.js';
124
124
  export * from './Theme.js';
125
125
  export * from './WebhookDefinition.js';
126
126
  export * from './DeviceGridLayoutDefinition.js';
127
+ export * from './GridBreakpointsDefinition.js';
127
128
  export * from './GridLayoutDefinition.js';
128
129
  export * from './GridSpacingDefinition.js';
130
+ export * from './ResponsiveGridLayoutDefinition.js';
131
+ export * from './NavbarDeviceGridLayoutDefinition.js';
132
+ export * from './NavbarLayoutDefinition.js';
129
133
  export * from './PageLayoutDefinition.js';
130
134
  //# sourceMappingURL=index.js.map
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 PageLayoutDefinition } from './gridLayout.js';
12
+ import { 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
  export interface AppDefinition {
@@ -118,6 +118,24 @@ export interface AppDefinition {
118
118
  * Only applies when `navigation` is set to `top`.
119
119
  */
120
120
  stackedHeader?: boolean;
121
+ /**
122
+ * Responsive grid layout for the top navigation header.
123
+ *
124
+ * Only applies when `navigation` is set to `top`. This cannot be combined with
125
+ * `stackedHeader`.
126
+ *
127
+ * The available template areas are logo, name, navigation, and controls.
128
+ * Each rendered area must be included and their first visual occurrence must follow that order.
129
+ */
130
+ navbar?: NavbarLayoutDefinition;
131
+ /**
132
+ * Minimum viewport widths in pixels at which the tablet and desktop grid layouts apply.
133
+ *
134
+ * Applies to every responsive grid layout in the app, including pages and the navbar. The
135
+ * mobile layout always applies from a width of 0. Each breakpoint must be smaller than the
136
+ * next.
137
+ */
138
+ breakpoints?: GridBreakpointsDefinition;
121
139
  /**
122
140
  * Whether to hide the group dropdown.
123
141
  *
@@ -316,6 +334,14 @@ export interface LoopPageDefinition extends BasePageDefinition {
316
334
  * @default true
317
335
  */
318
336
  retainFlowData?: boolean;
337
+ /**
338
+ * The page to display before the generated loop pages.
339
+ */
340
+ start?: SubPageDefinition;
341
+ /**
342
+ * The page to display after the generated loop pages.
343
+ */
344
+ end?: SubPageDefinition;
319
345
  }
320
346
  export interface AlternateTabsDefinition {
321
347
  foreach: SubPageDefinition;
@@ -8,12 +8,26 @@ export interface GridLayoutSpacingDefinition {
8
8
  padding: number;
9
9
  gap: number;
10
10
  }
11
+ export interface GridBreakpointsDefinition {
12
+ tablet?: number;
13
+ desktop?: number;
14
+ }
11
15
  export interface DeviceGridLayoutDefinition {
16
+ layout?: GridLayoutDefinition;
17
+ spacing?: GridLayoutSpacingDefinition;
18
+ }
19
+ export interface ResponsiveGridLayoutDefinition {
20
+ desktop?: DeviceGridLayoutDefinition;
21
+ mobile?: DeviceGridLayoutDefinition;
22
+ tablet?: DeviceGridLayoutDefinition;
23
+ }
24
+ export interface NavbarDeviceGridLayoutDefinition extends DeviceGridLayoutDefinition {
12
25
  layout: GridLayoutDefinition;
13
- spacing: GridLayoutSpacingDefinition;
14
26
  }
15
- export type PageLayoutDefinition = RequireAtLeastOne<{
16
- mobile: DeviceGridLayoutDefinition;
17
- tablet: DeviceGridLayoutDefinition;
18
- desktop: DeviceGridLayoutDefinition;
19
- }>;
27
+ export interface ResponsiveNavbarGridLayoutDefinition {
28
+ desktop?: NavbarDeviceGridLayoutDefinition;
29
+ mobile?: NavbarDeviceGridLayoutDefinition;
30
+ tablet?: NavbarDeviceGridLayoutDefinition;
31
+ }
32
+ export type NavbarLayoutDefinition = RequireAtLeastOne<ResponsiveNavbarGridLayoutDefinition>;
33
+ export type PageLayoutDefinition = RequireAtLeastOne<ResponsiveGridLayoutDefinition>;
package/validation.js CHANGED
@@ -437,7 +437,7 @@ function validateTemplateAreasAreRectangular(template, report, path) {
437
437
  }
438
438
  }
439
439
  }
440
- function validatePageLayoutDefinition(layoutDefinition, report, path) {
440
+ function validateResponsiveGridLayoutDefinition(layoutDefinition, report, path) {
441
441
  if (!layoutDefinition) {
442
442
  return;
443
443
  }
@@ -445,10 +445,16 @@ function validatePageLayoutDefinition(layoutDefinition, report, path) {
445
445
  if (!deviceDefinition?.layout?.template || !deviceDefinition?.layout?.columns) {
446
446
  continue;
447
447
  }
448
- if (deviceDefinition.layout.template.some((row) => row.split(' ').length !== deviceDefinition.layout.columns)) {
449
- report(deviceDefinition.layout.template, 'template needs to be the same length as number of columns', [...path, deviceName, 'layout', 'template']);
448
+ const { layout } = deviceDefinition;
449
+ if (layout.template.some((row) => row.split(' ').length !== layout.columns)) {
450
+ report(layout.template, 'template needs to be the same length as number of columns', [
451
+ ...path,
452
+ deviceName,
453
+ 'layout',
454
+ 'template',
455
+ ]);
450
456
  }
451
- validateTemplateAreasAreRectangular(deviceDefinition.layout.template, report, [
457
+ validateTemplateAreasAreRectangular(layout.template, report, [
452
458
  ...path,
453
459
  deviceName,
454
460
  'layout',
@@ -465,12 +471,67 @@ function validateBlockGridAreas(blocks, layoutDefinition, report, path) {
465
471
  }
466
472
  }
467
473
  function validateSubPageLayout(layout, blocks, report, path) {
468
- validatePageLayoutDefinition(layout, report, path);
474
+ validateResponsiveGridLayoutDefinition(layout, report, path);
469
475
  if (layout && blocks) {
470
476
  validateBlockGridAreas(blocks, layout, report, [...path, 'blocks']);
471
477
  }
472
478
  }
473
479
  function validateGridLayout(definition, report) {
480
+ const breakpoints = definition.layout?.breakpoints;
481
+ if (breakpoints?.tablet != null &&
482
+ breakpoints?.desktop != null &&
483
+ breakpoints.tablet >= breakpoints.desktop) {
484
+ report(breakpoints, 'tablet breakpoint must be smaller than the desktop breakpoint', [
485
+ 'layout',
486
+ 'breakpoints',
487
+ ]);
488
+ }
489
+ const navbarLayout = definition.layout?.navbar;
490
+ validateResponsiveGridLayoutDefinition(navbarLayout, report, ['layout', 'navbar']);
491
+ if (navbarLayout) {
492
+ if (definition.layout?.navigation !== 'top') {
493
+ report(navbarLayout, 'only applies when navigation is set to top', ['layout', 'navbar']);
494
+ }
495
+ if (definition.layout?.stackedHeader) {
496
+ report(navbarLayout, 'cannot be combined with stackedHeader', ['layout', 'navbar']);
497
+ }
498
+ const navbarAreaOrder = ['logo', 'name', 'navigation', 'controls'];
499
+ const allowedAreas = new Set(['.', ...navbarAreaOrder]);
500
+ const requiredAreas = definition.layout?.logo?.position === 'navbar' ? navbarAreaOrder : navbarAreaOrder.slice(1);
501
+ for (const [deviceName, deviceDefinition] of Object.entries(navbarLayout)) {
502
+ if (!deviceDefinition?.layout?.template) {
503
+ continue;
504
+ }
505
+ const { template } = deviceDefinition.layout;
506
+ const templatePath = ['layout', 'navbar', deviceName, 'layout', 'template'];
507
+ const visualAreaOrder = [];
508
+ const visualAreas = new Set();
509
+ for (const [rowIndex, row] of deviceDefinition.layout.template.entries()) {
510
+ for (const area of row.split(' ')) {
511
+ if (!allowedAreas.has(area)) {
512
+ report(row, `unknown navbar grid area '${area}'`, [...templatePath, rowIndex]);
513
+ }
514
+ else if (area !== '.' && !visualAreas.has(area)) {
515
+ visualAreas.add(area);
516
+ visualAreaOrder.push(area);
517
+ }
518
+ }
519
+ }
520
+ let hasMissingArea = false;
521
+ for (const area of requiredAreas) {
522
+ if (!visualAreas.has(area)) {
523
+ hasMissingArea = true;
524
+ report(template, `navbar grid is missing required area '${area}'`, templatePath);
525
+ }
526
+ }
527
+ if (!hasMissingArea) {
528
+ const expectedAreaOrder = navbarAreaOrder.filter((area) => visualAreas.has(area));
529
+ if (visualAreaOrder.some((area, index) => area !== expectedAreaOrder[index])) {
530
+ report(template, `navbar grid areas must follow the accessible order: ${expectedAreaOrder.join(', ')}`, templatePath);
531
+ }
532
+ }
533
+ }
534
+ }
474
535
  iterApp(definition, {
475
536
  onPage(page, path) {
476
537
  // Basic page
@@ -503,10 +564,19 @@ function validateGridLayout(definition, report) {
503
564
  // Loop page
504
565
  if (page.type === 'loop') {
505
566
  const loopPage = page;
567
+ if (loopPage.start) {
568
+ validateSubPageLayout(loopPage.start.layout, loopPage.start.blocks, report, [
569
+ ...path,
570
+ 'start',
571
+ ]);
572
+ }
506
573
  validateSubPageLayout(loopPage.foreach.layout, loopPage.foreach.blocks, report, [
507
574
  ...path,
508
575
  'foreach',
509
576
  ]);
577
+ if (loopPage.end) {
578
+ validateSubPageLayout(loopPage.end.layout, loopPage.end.blocks, report, [...path, 'end']);
579
+ }
510
580
  }
511
581
  },
512
582
  });