@jskit-ai/jskit-catalog 0.2.28 → 0.2.30

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.
@@ -6,54 +6,54 @@
6
6
  "release": {
7
7
  "packages": {
8
8
  "@jskit-ai/agent": "0.1.1",
9
- "@jskit-ai/agent-docs": "0.1.276",
9
+ "@jskit-ai/agent-docs": "0.1.278",
10
10
  "@jskit-ai/agent-runtime": "0.1.2",
11
11
  "@jskit-ai/architecture-inspector": "0.1.8",
12
- "@jskit-ai/assistant-core": "0.2.22",
13
- "@jskit-ai/assistant-runtime": "0.2.24",
14
- "@jskit-ai/assistant-voice": "0.2.24",
15
- "@jskit-ai/assistant-voice-sherpa": "0.1.71",
12
+ "@jskit-ai/assistant-core": "0.2.23",
13
+ "@jskit-ai/assistant-runtime": "0.2.25",
14
+ "@jskit-ai/assistant-voice": "0.2.25",
15
+ "@jskit-ai/assistant-voice-sherpa": "0.1.72",
16
16
  "@jskit-ai/auth-core": "0.2.10",
17
17
  "@jskit-ai/auth-provider-local-core": "0.2.12",
18
18
  "@jskit-ai/auth-provider-local-db-core": "0.1.153",
19
- "@jskit-ai/auth-provider-oidc-core": "0.2.15",
19
+ "@jskit-ai/auth-provider-oidc-core": "0.2.16",
20
20
  "@jskit-ai/auth-provider-supabase-core": "0.2.12",
21
- "@jskit-ai/auth-web": "0.2.13",
21
+ "@jskit-ai/auth-web": "0.2.14",
22
22
  "@jskit-ai/config-eslint": "0.1.245",
23
23
  "@jskit-ai/connector-google-calendar": "0.2.11",
24
24
  "@jskit-ai/connectors-catalog": "0.2.11",
25
25
  "@jskit-ai/connectors-core": "0.2.11",
26
26
  "@jskit-ai/connectors-web": "0.2.11",
27
27
  "@jskit-ai/console-core": "0.2.12",
28
- "@jskit-ai/console-web": "0.1.235",
28
+ "@jskit-ai/console-web": "0.1.236",
29
29
  "@jskit-ai/crud-core": "0.2.11",
30
30
  "@jskit-ai/database-runtime": "0.1.259",
31
31
  "@jskit-ai/database-runtime-mysql": "0.1.257",
32
32
  "@jskit-ai/database-runtime-postgres": "0.1.256",
33
33
  "@jskit-ai/http-runtime": "0.2.10",
34
- "@jskit-ai/http-web": "0.2.16",
35
- "@jskit-ai/jskit-catalog": "0.2.28",
34
+ "@jskit-ai/http-web": "0.2.17",
35
+ "@jskit-ai/jskit-catalog": "0.2.30",
36
36
  "@jskit-ai/json-rest-api-core": "0.2.11",
37
37
  "@jskit-ai/kernel": "0.2.10",
38
- "@jskit-ai/material-web": "0.1.0",
39
- "@jskit-ai/mobile-capacitor": "0.2.11",
38
+ "@jskit-ai/material-web": "0.1.1",
39
+ "@jskit-ai/mobile-capacitor": "0.2.12",
40
40
  "@jskit-ai/oidc-issuer-core": "0.1.17",
41
41
  "@jskit-ai/payments-core": "0.2.1",
42
42
  "@jskit-ai/payments-web": "0.2.10",
43
- "@jskit-ai/realtime": "0.2.14",
43
+ "@jskit-ai/realtime": "0.2.15",
44
44
  "@jskit-ai/release": "0.1.9",
45
45
  "@jskit-ai/resource-core": "0.1.201",
46
46
  "@jskit-ai/resource-crud-core": "0.1.201",
47
47
  "@jskit-ai/rewarded-core": "0.2.12",
48
- "@jskit-ai/rewarded-web": "0.2.13",
49
- "@jskit-ai/shell-web": "0.2.11",
48
+ "@jskit-ai/rewarded-web": "0.2.14",
49
+ "@jskit-ai/shell-web": "0.2.12",
50
50
  "@jskit-ai/storage-runtime": "0.1.256",
51
51
  "@jskit-ai/uploads-image-web": "0.2.10",
52
52
  "@jskit-ai/uploads-runtime": "0.2.10",
53
53
  "@jskit-ai/users-core": "0.2.12",
54
- "@jskit-ai/users-web": "0.2.18",
54
+ "@jskit-ai/users-web": "0.2.19",
55
55
  "@jskit-ai/workspaces-core": "0.2.12",
56
- "@jskit-ai/workspaces-web": "0.2.18"
56
+ "@jskit-ai/workspaces-web": "0.2.19"
57
57
  },
58
58
  "singletonPackages": [
59
59
  "@jskit-ai/assistant-runtime",
@@ -179,7 +179,7 @@
179
179
  },
180
180
  {
181
181
  "packageId": "@jskit-ai/assistant-core",
182
- "version": "0.2.22",
182
+ "version": "0.2.23",
183
183
  "description": "Reusable assistant client/server/shared primitives without surface-specific routes or settings ownership.",
184
184
  "jskit": {
185
185
  "kind": "runtime",
@@ -364,7 +364,7 @@
364
364
  },
365
365
  {
366
366
  "packageId": "@jskit-ai/assistant-runtime",
367
- "version": "0.2.24",
367
+ "version": "0.2.25",
368
368
  "description": "Capability-based assistant runtime with per-surface chat, transcripts, settings, and action tools.",
369
369
  "jskit": {
370
370
  "kind": "runtime",
@@ -458,15 +458,15 @@
458
458
  },
459
459
  "packageJson": {
460
460
  "dependencies": {
461
- "@jskit-ai/assistant-core": "0.2.22",
461
+ "@jskit-ai/assistant-core": "0.2.23",
462
462
  "json-rest-schema": "^1.0.17"
463
463
  },
464
464
  "peerDependencies": {
465
465
  "@jskit-ai/http-runtime": "0.2.10",
466
- "@jskit-ai/http-web": "0.2.16",
466
+ "@jskit-ai/http-web": "0.2.17",
467
467
  "@jskit-ai/kernel": "0.2.10",
468
- "@jskit-ai/realtime": "0.2.14",
469
- "@jskit-ai/shell-web": "0.2.11",
468
+ "@jskit-ai/realtime": "0.2.15",
469
+ "@jskit-ai/shell-web": "0.2.12",
470
470
  "@tanstack/vue-query": "^5.90.5",
471
471
  "vue": "^3.5.13",
472
472
  "vuetify": "^4.0.0",
@@ -476,7 +476,7 @@
476
476
  },
477
477
  {
478
478
  "packageId": "@jskit-ai/assistant-voice",
479
- "version": "0.2.24",
479
+ "version": "0.2.25",
480
480
  "description": "Duplex voice conversations with application-owned targets, authorization and speech services.",
481
481
  "jskit": {
482
482
  "kind": "runtime",
@@ -518,7 +518,7 @@
518
518
  },
519
519
  "packageJson": {
520
520
  "dependencies": {
521
- "@jskit-ai/assistant-core": "0.2.22",
521
+ "@jskit-ai/assistant-core": "0.2.23",
522
522
  "ws": "^8.20.1"
523
523
  },
524
524
  "peerDependencies": {
@@ -530,7 +530,7 @@
530
530
  },
531
531
  {
532
532
  "packageId": "@jskit-ai/assistant-voice-sherpa",
533
- "version": "0.1.71",
533
+ "version": "0.1.72",
534
534
  "description": "Optional native Sherpa speech engine and explicit model preparation for assistant voice.",
535
535
  "jskit": {
536
536
  "kind": "runtime",
@@ -568,7 +568,7 @@
568
568
  },
569
569
  "packageJson": {
570
570
  "dependencies": {
571
- "@jskit-ai/assistant-voice": "0.2.24",
571
+ "@jskit-ai/assistant-voice": "0.2.25",
572
572
  "sherpa-onnx-node": "1.13.6"
573
573
  }
574
574
  }
@@ -814,7 +814,7 @@
814
814
  },
815
815
  {
816
816
  "packageId": "@jskit-ai/auth-provider-oidc-core",
817
- "version": "0.2.15",
817
+ "version": "0.2.16",
818
818
  "description": "Sign in through any OpenID Connect issuer: self-registration or a static client, app-owned sessions, and back-channel logout.",
819
819
  "jskit": {
820
820
  "kind": "runtime",
@@ -953,7 +953,7 @@
953
953
  },
954
954
  {
955
955
  "packageId": "@jskit-ai/auth-web",
956
- "version": "0.2.13",
956
+ "version": "0.2.14",
957
957
  "description": "Auth web module: Fastify auth routes plus web login/sign-out scaffolds.",
958
958
  "jskit": {
959
959
  "kind": "runtime",
@@ -1228,7 +1228,7 @@
1228
1228
  "@jskit-ai/auth-core": "0.2.10",
1229
1229
  "@jskit-ai/http-runtime": "0.2.10",
1230
1230
  "@jskit-ai/kernel": "0.2.10",
1231
- "@jskit-ai/shell-web": "0.2.11",
1231
+ "@jskit-ai/shell-web": "0.2.12",
1232
1232
  "@tanstack/vue-query": "^5.90.5",
1233
1233
  "pinia": "^4.0.3",
1234
1234
  "vue": "^3.5.44",
@@ -1480,7 +1480,7 @@
1480
1480
  },
1481
1481
  {
1482
1482
  "packageId": "@jskit-ai/console-web",
1483
- "version": "0.1.235",
1483
+ "version": "0.1.236",
1484
1484
  "description": "Authenticated console surface scaffold and surface policy wiring.",
1485
1485
  "jskit": {
1486
1486
  "kind": "runtime",
@@ -1585,8 +1585,8 @@
1585
1585
  "@jskit-ai/console-core": "0.2.12"
1586
1586
  },
1587
1587
  "peerDependencies": {
1588
- "@jskit-ai/auth-web": "0.2.13",
1589
- "@jskit-ai/shell-web": "0.2.11"
1588
+ "@jskit-ai/auth-web": "0.2.14",
1589
+ "@jskit-ai/shell-web": "0.2.12"
1590
1590
  }
1591
1591
  }
1592
1592
  },
@@ -1842,7 +1842,7 @@
1842
1842
  },
1843
1843
  {
1844
1844
  "packageId": "@jskit-ai/http-web",
1845
- "version": "0.2.16",
1845
+ "version": "0.2.17",
1846
1846
  "description": "Neutral Vue request, command, resource, and CRUD UI runtime for JSKIT web applications.",
1847
1847
  "jskit": {
1848
1848
  "kind": "runtime",
@@ -1921,8 +1921,8 @@
1921
1921
  "peerDependencies": {
1922
1922
  "@jskit-ai/http-runtime": "0.2.10",
1923
1923
  "@jskit-ai/kernel": "0.2.10",
1924
- "@jskit-ai/realtime": "0.2.14",
1925
- "@jskit-ai/shell-web": "0.2.11",
1924
+ "@jskit-ai/realtime": "0.2.15",
1925
+ "@jskit-ai/shell-web": "0.2.12",
1926
1926
  "@tanstack/vue-query": "^5.90.5",
1927
1927
  "vue": "^3.5.13",
1928
1928
  "vue-router": "^5.0.4",
@@ -1986,7 +1986,7 @@
1986
1986
  },
1987
1987
  {
1988
1988
  "packageId": "@jskit-ai/material-web",
1989
- "version": "0.1.0",
1989
+ "version": "0.1.1",
1990
1990
  "description": "Exact Material 3 for JSKIT Vuetify apps: seed-generated light and dark schemes, Material Symbols Rounded icon aliases, Roboto Flex and Material 3 component defaults.",
1991
1991
  "jskit": {
1992
1992
  "kind": "runtime",
@@ -2015,7 +2015,7 @@
2015
2015
  },
2016
2016
  {
2017
2017
  "packageId": "@jskit-ai/mobile-capacitor",
2018
- "version": "0.2.11",
2018
+ "version": "0.2.12",
2019
2019
  "description": "Thin Capacitor client integration for JSKIT mobile-shell launch routing and auth callback completion.",
2020
2020
  "jskit": {
2021
2021
  "kind": "runtime",
@@ -2067,7 +2067,7 @@
2067
2067
  },
2068
2068
  "peerDependencies": {
2069
2069
  "@jskit-ai/kernel": "0.2.10",
2070
- "@jskit-ai/shell-web": "0.2.11"
2070
+ "@jskit-ai/shell-web": "0.2.12"
2071
2071
  }
2072
2072
  }
2073
2073
  },
@@ -2258,7 +2258,7 @@
2258
2258
  },
2259
2259
  {
2260
2260
  "packageId": "@jskit-ai/realtime",
2261
- "version": "0.2.14",
2261
+ "version": "0.2.15",
2262
2262
  "description": "Thin, generic realtime runtime wrappers for socket.io server and client.",
2263
2263
  "jskit": {
2264
2264
  "kind": "runtime",
@@ -2582,7 +2582,7 @@
2582
2582
  },
2583
2583
  {
2584
2584
  "packageId": "@jskit-ai/rewarded-web",
2585
- "version": "0.2.13",
2585
+ "version": "0.2.14",
2586
2586
  "description": "Rewarded gate client workflow with explicitly composed application delivery.",
2587
2587
  "jskit": {
2588
2588
  "kind": "runtime",
@@ -2636,14 +2636,14 @@
2636
2636
  "peerDependencies": {
2637
2637
  "@jskit-ai/http-runtime": "0.2.10",
2638
2638
  "@jskit-ai/kernel": "0.2.10",
2639
- "@jskit-ai/shell-web": "0.2.11",
2639
+ "@jskit-ai/shell-web": "0.2.12",
2640
2640
  "vue": "^3.5.13"
2641
2641
  }
2642
2642
  }
2643
2643
  },
2644
2644
  {
2645
2645
  "packageId": "@jskit-ai/shell-web",
2646
- "version": "0.2.11",
2646
+ "version": "0.2.12",
2647
2647
  "description": "Web shell layout runtime with outlet-based placement contributions.",
2648
2648
  "jskit": {
2649
2649
  "kind": "runtime",
@@ -2680,6 +2680,7 @@
2680
2680
  "@jskit-ai/shell-web/client/navigation/useSurfaceRouteContext",
2681
2681
  "@jskit-ai/shell-web/client/navigation/useShellNavigation",
2682
2682
  "@jskit-ai/shell-web/client/navigation/documentNavigation",
2683
+ "@jskit-ai/shell-web/client/composables/useShellAppBar",
2683
2684
  "@jskit-ai/shell-web/client/composables/useShellLayoutState",
2684
2685
  "@jskit-ai/shell-web/client/composables/useShellPageTitle",
2685
2686
  "@jskit-ai/shell-web/client/composables/useShellWindowSizeClass",
@@ -3403,7 +3404,7 @@
3403
3404
  },
3404
3405
  {
3405
3406
  "packageId": "@jskit-ai/users-web",
3406
- "version": "0.2.18",
3407
+ "version": "0.2.19",
3407
3408
  "description": "Users web module with account, profile, and user-specific shell UI.",
3408
3409
  "jskit": {
3409
3410
  "kind": "runtime",
@@ -3612,11 +3613,11 @@
3612
3613
  "@jskit-ai/users-core": "0.2.12"
3613
3614
  },
3614
3615
  "peerDependencies": {
3615
- "@jskit-ai/auth-web": "0.2.13",
3616
+ "@jskit-ai/auth-web": "0.2.14",
3616
3617
  "@jskit-ai/http-runtime": "0.2.10",
3617
- "@jskit-ai/http-web": "0.2.16",
3618
+ "@jskit-ai/http-web": "0.2.17",
3618
3619
  "@jskit-ai/kernel": "0.2.10",
3619
- "@jskit-ai/shell-web": "0.2.11",
3620
+ "@jskit-ai/shell-web": "0.2.12",
3620
3621
  "@tanstack/vue-query": "^5.90.5",
3621
3622
  "vue": "^3.5.13",
3622
3623
  "vue-router": "^5.0.4",
@@ -3813,7 +3814,7 @@
3813
3814
  },
3814
3815
  {
3815
3816
  "packageId": "@jskit-ai/workspaces-web",
3816
- "version": "0.2.18",
3817
+ "version": "0.2.19",
3817
3818
  "description": "Workspace web module: workspace selector, tools widget, workspace surfaces, members UI, and settings hosts.",
3818
3819
  "jskit": {
3819
3820
  "kind": "runtime",
@@ -4065,11 +4066,11 @@
4065
4066
  "@jskit-ai/workspaces-core": "0.2.12"
4066
4067
  },
4067
4068
  "peerDependencies": {
4068
- "@jskit-ai/auth-web": "0.2.13",
4069
- "@jskit-ai/http-web": "0.2.16",
4069
+ "@jskit-ai/auth-web": "0.2.14",
4070
+ "@jskit-ai/http-web": "0.2.17",
4070
4071
  "@jskit-ai/kernel": "0.2.10",
4071
- "@jskit-ai/shell-web": "0.2.11",
4072
- "@jskit-ai/users-web": "0.2.18",
4072
+ "@jskit-ai/shell-web": "0.2.12",
4073
+ "@jskit-ai/users-web": "0.2.19",
4073
4074
  "@tanstack/vue-query": "^5.90.5",
4074
4075
  "vue": "^3.5.13",
4075
4076
  "vue-router": "^5.0.4",
@@ -4101,7 +4102,7 @@
4101
4102
  "@jskit-ai/shell-web"
4102
4103
  ],
4103
4104
  "packageId": "@jskit-ai/assistant-runtime",
4104
- "packageVersion": "0.2.24",
4105
+ "packageVersion": "0.2.25",
4105
4106
  "documentPath": "patterns/assistant-surface/PATTERN.md",
4106
4107
  "examplePath": "patterns/assistant-surface/example",
4107
4108
  "files": [
@@ -4163,7 +4164,7 @@
4163
4164
  "@jskit-ai/shell-web"
4164
4165
  ],
4165
4166
  "packageId": "@jskit-ai/auth-web",
4166
- "packageVersion": "0.2.13",
4167
+ "packageVersion": "0.2.14",
4167
4168
  "documentPath": "patterns/auth-surface/PATTERN.md",
4168
4169
  "examplePath": "patterns/auth-surface/example",
4169
4170
  "files": [
@@ -4201,7 +4202,7 @@
4201
4202
  "@jskit-ai/auth-web"
4202
4203
  ],
4203
4204
  "packageId": "@jskit-ai/auth-provider-oidc-core",
4204
- "packageVersion": "0.2.15",
4205
+ "packageVersion": "0.2.16",
4205
4206
  "documentPath": "patterns/oidc-sign-in/PATTERN.md",
4206
4207
  "examplePath": "patterns/oidc-sign-in/example",
4207
4208
  "files": [
@@ -4672,7 +4673,7 @@
4672
4673
  "@jskit-ai/shell-web"
4673
4674
  ],
4674
4675
  "packageId": "@jskit-ai/console-web",
4675
- "packageVersion": "0.1.235",
4676
+ "packageVersion": "0.1.236",
4676
4677
  "documentPath": "patterns/console-surface/PATTERN.md",
4677
4678
  "examplePath": "patterns/console-surface/example",
4678
4679
  "files": [
@@ -4705,7 +4706,7 @@
4705
4706
  "@jskit-ai/resource-crud-core"
4706
4707
  ],
4707
4708
  "packageId": "@jskit-ai/http-web",
4708
- "packageVersion": "0.2.16",
4709
+ "packageVersion": "0.2.17",
4709
4710
  "documentPath": "patterns/crud-screen-set/PATTERN.md",
4710
4711
  "examplePath": "patterns/crud-screen-set/example",
4711
4712
  "files": [
@@ -4849,7 +4850,7 @@
4849
4850
  "@jskit-ai/mobile-capacitor"
4850
4851
  ],
4851
4852
  "packageId": "@jskit-ai/mobile-capacitor",
4852
- "packageVersion": "0.2.11",
4853
+ "packageVersion": "0.2.12",
4853
4854
  "documentPath": "patterns/android-application/PATTERN.md",
4854
4855
  "examplePath": "patterns/android-application/example",
4855
4856
  "files": [
@@ -4873,7 +4874,7 @@
4873
4874
  "@jskit-ai/shell-web"
4874
4875
  ],
4875
4876
  "packageId": "@jskit-ai/realtime",
4876
- "packageVersion": "0.2.14",
4877
+ "packageVersion": "0.2.15",
4877
4878
  "documentPath": "patterns/realtime-application/PATTERN.md",
4878
4879
  "examplePath": "patterns/realtime-application/example",
4879
4880
  "files": [
@@ -4897,7 +4898,7 @@
4897
4898
  "@jskit-ai/rewarded-web"
4898
4899
  ],
4899
4900
  "packageId": "@jskit-ai/rewarded-web",
4900
- "packageVersion": "0.2.13",
4901
+ "packageVersion": "0.2.14",
4901
4902
  "documentPath": "patterns/google-rewarded/PATTERN.md",
4902
4903
  "examplePath": "patterns/google-rewarded/example",
4903
4904
  "files": [
@@ -4924,7 +4925,7 @@
4924
4925
  "@jskit-ai/shell-web"
4925
4926
  ],
4926
4927
  "packageId": "@jskit-ai/shell-web",
4927
- "packageVersion": "0.2.11",
4928
+ "packageVersion": "0.2.12",
4928
4929
  "documentPath": "patterns/application-shell/PATTERN.md",
4929
4930
  "examplePath": "patterns/application-shell/example",
4930
4931
  "files": [
@@ -4969,7 +4970,7 @@
4969
4970
  "@jskit-ai/shell-web"
4970
4971
  ],
4971
4972
  "packageId": "@jskit-ai/shell-web",
4972
- "packageVersion": "0.2.11",
4973
+ "packageVersion": "0.2.12",
4973
4974
  "documentPath": "patterns/page-and-placement/PATTERN.md",
4974
4975
  "examplePath": "patterns/page-and-placement/example",
4975
4976
  "files": [
@@ -5005,7 +5006,7 @@
5005
5006
  "@jskit-ai/users-web"
5006
5007
  ],
5007
5008
  "packageId": "@jskit-ai/users-web",
5008
- "packageVersion": "0.2.18",
5009
+ "packageVersion": "0.2.19",
5009
5010
  "documentPath": "patterns/account-settings/PATTERN.md",
5010
5011
  "examplePath": "patterns/account-settings/example",
5011
5012
  "files": [
@@ -5101,7 +5102,7 @@
5101
5102
  "@jskit-ai/workspaces-web"
5102
5103
  ],
5103
5104
  "packageId": "@jskit-ai/workspaces-web",
5104
- "packageVersion": "0.2.18",
5105
+ "packageVersion": "0.2.19",
5105
5106
  "documentPath": "patterns/workspace-surfaces/PATTERN.md",
5106
5107
  "examplePath": "patterns/workspace-surfaces/example",
5107
5108
  "files": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/jskit-catalog",
3
- "version": "0.2.28",
3
+ "version": "0.2.30",
4
4
  "description": "Published metadata catalog for JSKIT package metadata.",
5
5
  "type": "module",
6
6
  "files": [
@@ -216,7 +216,7 @@ function checkTemplate(template, context) {
216
216
  }
217
217
  if (SURFACE_TAGS.has(tag.name)) checkSurface(tag, report);
218
218
  if (tag.name === "v-btn") checkButtonSize(tag, report);
219
- checkBackControl(tag, report);
219
+ checkBackControl(tag, report, context.script);
220
220
  if (tag.name === "h1") {
221
221
  headings += 1;
222
222
  if (headings > 1) report("m3/duplicate-h1", tag.offset, "A second h1 in one template. A page has one title, carried by the top app bar.");
@@ -263,7 +263,9 @@ function checkFile(file, source, context) {
263
263
  const extension = path.extname(file);
264
264
  if (extension === ".vue") {
265
265
  const { template, scripts, styles } = splitVue(source);
266
- if (template) checkTemplate({ offset: template.offset, text: blankHtmlComments(template.text) }, fileContext);
266
+ // The template's handlers are defined in the component's script.
267
+ const script = scripts.map((block) => blankJsComments(block.text)).join("\n");
268
+ if (template) checkTemplate({ offset: template.offset, text: blankHtmlComments(template.text) }, { ...fileContext, script });
267
269
  for (const block of scripts) checkScript(blankJsComments(block.text), block.offset, fileContext);
268
270
  for (const block of styles) checkCss(blankCssComments(block.text), block.offset, fileContext);
269
271
  } else if (extension === ".css" || extension === ".scss") {
@@ -29,7 +29,7 @@ const BOUND_PATH = /^\s*(?:\{\s*path\s*:\s*)?(["'`])(\/[^"'`]*)/u;
29
29
  // A back handler or back component: goBack, onBack, handleBackClick, useBackNavigation, PageBackButton, backToList.
30
30
  const BACK_NAME = String.raw`(?:[A-Za-z][\w$]*Back(?:[A-Z][\w$]*)?|Back[A-Z][\w$]*|back(?:To[A-Z][\w$]*)?)`;
31
31
  const BACK_IDENTIFIER = new RegExp(String.raw`(?<![\w$])${BACK_NAME}(?![\w$])`, "u");
32
- const BACK_DEFINITION = new RegExp(String.raw`\b(?:function\s*\*?\s*|const\s+|let\s+|var\s+|import\s+)(${BACK_NAME})(?![\w$])|\bimport\s*\{[^}]*?(?<![\w$])(${BACK_NAME})(?![\w$])[^}]*\}`, "gu");
32
+ const BACK_DEFINITION = new RegExp(String.raw`\b(?:function\s*\*?\s*|const\s+|let\s+|var\s+)(${BACK_NAME})(?![\w$])`, "gu");
33
33
  const BACK_TEXT = /^\s*(?:[←‹«]|(?:go\s+)?back\b|return to\b)|\bback to\b/iu;
34
34
  const BACK_KEY = /\bback(?:To)?\b/iu;
35
35
  const ARROW_ICON = /arrow[-_]?back|arrow[-_]?left|keyboard[-_]?backspace|keyboard[-_]?return/iu;
@@ -40,10 +40,27 @@ const LABEL_ATTRIBUTES = new Set(["aria-label", "title", "label", "text"]);
40
40
  const ICON_ATTRIBUTES = new Set(["icon", "prepend-icon", "append-icon"]);
41
41
  const LINK_ATTRIBUTES = new Set(["to", "href"]);
42
42
  const NAVIGATES = /(?<![\w$])(?:[\w$]*router|useRouter\(\s*\))\s*\.\s*(?:push|replace)\s*\(|\.page\s*\(|resolve\w*Url\s*\(/iu;
43
+ // App navigation in a handler or a function: the router, JSKIT paths, the shell's up() and finish(), a RouterLink's
44
+ // navigate(), and the browser's own history and location.
45
+ const APP_NAVIGATION = new RegExp([
46
+ NAVIGATES.source,
47
+ ROUTER_HISTORY_CALL.source,
48
+ HISTORY_CALL.source,
49
+ LOCATION_WRITE.source,
50
+ String.raw`(?<![\w$])(?:up|finish|navigate)\s*\(`,
51
+ String.raw`\.\s*(?:up|finish)\s*\(`
52
+ ].join("|"), "iu");
53
+ // An embedded frame's history and location (a preview browser's Back and Forward) are the frame's, not the app's.
54
+ const FRAME_NAVIGATION = /\bcontentWindow\s*\??\.\s*(?:history|location)(?:\s*\??\.\s*[\w$]+)*/gu;
55
+ const CALLED_NAME = /(?<![\w$.])([A-Za-z_$][\w$]*)\s*\(/gu;
56
+ const UP_OWNER = "Up belongs to the shell's top app bar: ShellLayout's, or ShellUpButton in a bar the layout draws itself";
43
57
  // Main navigation: the navigation bar, rail and drawer render shell.primary-nav and shell.secondary-nav links.
44
58
  const MAIN_NAV_TARGET = /\btarget\s*:\s*["']shell\.(?:primary|secondary)-nav["']/gu;
45
- // A layout that imports the app's ShellLayout wrapper or shell-web's ShellLayout renders the top app bar.
59
+ // A layout that imports the app's ShellLayout wrapper or shell-web's ShellLayout renders the top app bar; one that
60
+ // draws its own bar renders shell-web's Up control in it.
46
61
  const SHELL_LAYOUT_IMPORT = /\bfrom\s*["'](?:@\/components\/ShellLayout|@jskit-ai\/shell-web\/client\/components\/ShellLayout)(?:\.vue)?["']/u;
62
+ const SHELL_UP_TAG = /<\s*(?:ShellUpButton|shell-up-button)\b/u;
63
+ const LOCAL_IMPORT = /\bfrom\s*["']((?:@\/|\.{1,2}\/)[^"']+)["']/gu;
47
64
  const ROUTER_VIEW_TAG = /<\s*(?:RouterView|router-view)\b/u;
48
65
 
49
66
  function isInAppPath(value) {
@@ -84,13 +101,68 @@ function isPlacementLink(code, offset) {
84
101
  return Boolean(placement) && /\btarget\s*:\s*["'`][\w.-]+["'`]/u.test(placement.text) && /\bkind\s*:/u.test(placement.text);
85
102
  }
86
103
 
104
+ // A back-named function that navigates the app; a variable, an import or an in-component step is not back logic.
87
105
  function checkScriptBackNames(code, offset, report) {
88
106
  for (const match of code.matchAll(BACK_DEFINITION)) {
89
- const name = match[1] || match[2];
90
- report("nav/back-control", offset + match.index, `${name} is back logic in the app. Up belongs to the shell; delete it and declare the page's parent.`);
107
+ if (!navigatesApp(definitionOf(code, match[1]), code)) continue;
108
+ report("nav/back-control", offset + match.index, `${match[1]} navigates back in the app. ${UP_OWNER}; delete it and declare the page's parent.`);
91
109
  }
92
110
  }
93
111
 
112
+ // The text of a local function's definition: function name(...) { ... }, or const name = ... to its end.
113
+ function definitionOf(script, name) {
114
+ const escaped = name.replace(/\$/gu, "\\$");
115
+ const declaration = new RegExp(String.raw`\bfunction\s*\*?\s*${escaped}\s*\(`, "u").exec(script);
116
+ if (declaration) {
117
+ const parameters = balancedFrom(script, declaration.index + declaration[0].length - 1, "(", ")");
118
+ return parameters ? balancedFrom(script, parameters.end)?.text || "" : "";
119
+ }
120
+ const binding = new RegExp(String.raw`\b(?:const|let|var)\s+${escaped}\s*=`, "u").exec(script);
121
+ return binding ? statementFrom(script, binding.index + binding[0].length) : "";
122
+ }
123
+
124
+ // An initialiser up to its statement's end: a semicolon or a line break outside brackets that ends the expression.
125
+ function statementFrom(text, start) {
126
+ let depth = 0;
127
+ for (let index = start; index < text.length; index += 1) {
128
+ const char = text[index];
129
+ if ("([{".includes(char)) depth += 1;
130
+ else if (")]}".includes(char)) {
131
+ if (depth === 0) return text.slice(start, index);
132
+ depth -= 1;
133
+ } else if (depth === 0 && (char === ";" || char === "\n")) {
134
+ const sofar = text.slice(start, index);
135
+ if (char === ";" || (sofar.trim() && !/(?:=>|[=(,?:&|+*/-])\s*$/u.test(sofar))) return sofar;
136
+ }
137
+ }
138
+ return text.slice(start);
139
+ }
140
+
141
+ // Whether code navigates the app, directly or through the functions it calls that the component defines.
142
+ function navigatesApp(code, script, seen = new Set()) {
143
+ const own = code.replace(FRAME_NAVIGATION, "");
144
+ if (APP_NAVIGATION.test(own)) return true;
145
+ for (const match of own.matchAll(CALLED_NAME)) {
146
+ if (seen.has(match[1])) continue;
147
+ seen.add(match[1]);
148
+ const definition = definitionOf(script, match[1]);
149
+ if (definition && navigatesApp(definition, script, seen)) return true;
150
+ }
151
+ return false;
152
+ }
153
+
154
+ // A control navigates the app when it links into it (to, or an in-app href) or one of its handlers navigates; a bare
155
+ // handler name is a call. "Back to top" (href="#top") scrolls the page.
156
+ function navigatesFromControl(tag, script) {
157
+ return tag.attributes.some((entry) => {
158
+ if (entry.name === "to") return true;
159
+ if (entry.name === "href") return entry.bound || isInAppPath(entry.value);
160
+ if (!entry.event) return false;
161
+ const handler = entry.value.trim();
162
+ return navigatesApp(/^[\w$.]+$/u.test(handler) ? `${handler}()` : handler, script);
163
+ });
164
+ }
165
+
94
166
  function attribute(tag, name) {
95
167
  return tag.attributes.find((entry) => entry.name === name);
96
168
  }
@@ -108,33 +180,35 @@ function looksLikeBack(tag) {
108
180
  const mustaches = [...tag.inner.matchAll(/\{\{([\s\S]*?)\}\}/gu)].map((match) => match[1]);
109
181
  const labels = tag.attributes.filter((entry) => LABEL_ATTRIBUTES.has(entry.name));
110
182
  const icons = tag.attributes.filter((entry) => ICON_ATTRIBUTES.has(entry.name)).map((entry) => entry.value);
111
- const navigates = tag.attributes.some((entry) => LINK_ATTRIBUTES.has(entry.name) || (entry.event && NAVIGATES.test(entry.value)));
112
183
  const iconText = [...icons, tag.inner].join(" ");
113
184
  return BACK_TEXT.test(visibleText(tag.inner)) ||
114
185
  mustaches.some((expression) => BACK_KEY.test(expression)) ||
115
186
  labels.some((entry) => (entry.bound ? BACK_KEY : BACK_TEXT).test(entry.value)) ||
116
187
  ARROW_ICON.test(iconText) ||
117
- (navigates && CHEVRON_ICON.test(iconText));
188
+ CHEVRON_ICON.test(iconText);
118
189
  }
119
190
 
120
- // Every back control but the shell's Up arrow: "Back to X", arrows and navigating chevrons, breadcrumbs, back
121
- // components and back handlers.
122
- function checkBackControl(tag, report) {
191
+ // Every back control but the shell's Up arrow: a control that navigates the app and reads as back ("Back to X", an
192
+ // arrow or chevron, a back component or handler), and breadcrumbs. Prose, a variable, an in-component step and an
193
+ // embedded frame's own Back and Forward navigate nothing in the app. `script` is the component's script, where its
194
+ // handlers are defined.
195
+ function checkBackControl(tag, report, script = "") {
123
196
  if (BREADCRUMB_TAGS.has(tag.name)) {
124
197
  report("nav/back-control", tag.offset, "Breadcrumbs navigate back up the hierarchy. The shell's Up arrow does that; the app bar title says where the person is.");
125
198
  return;
126
199
  }
200
+ if (!navigatesFromControl(tag, script)) return;
127
201
  if (isBackTag(tag)) {
128
- report("nav/back-control", tag.offset, `<${tag.name}> is a back control. Up belongs to the shell; remove it and declare the page's parent.`);
202
+ report("nav/back-control", tag.offset, `<${tag.name}> is a back control. ${UP_OWNER}; remove it and declare the page's parent.`);
129
203
  return;
130
204
  }
131
205
  const handler = tag.attributes.find((entry) => entry.event && BACK_IDENTIFIER.test(entry.value));
132
206
  if (handler) {
133
- report("nav/back-control", handler.offset, `${handler.raw}="${handler.value.trim()}" is a back handler. Up belongs to the shell; remove the control and declare the page's parent.`);
207
+ report("nav/back-control", handler.offset, `${handler.raw}="${handler.value.trim()}" is a back handler. ${UP_OWNER}; remove the control and declare the page's parent.`);
134
208
  return;
135
209
  }
136
210
  if (CLICKABLE_TAGS.has(tag.name) && looksLikeBack(tag)) {
137
- report("nav/back-control", tag.offset, "A back control in the page. Up appears once, in the top app bar, from the trail or the declared parent; remove this one.");
211
+ report("nav/back-control", tag.offset, `A back control in the page. ${UP_OWNER}, from the trail or the declared parent; remove this one.`);
138
212
  }
139
213
  }
140
214
 
@@ -254,10 +328,39 @@ function readPage(file, source) {
254
328
  redirect: REDIRECT_KEY.test(meta),
255
329
  parent: declarations.find(({ text }) => PARENT_KEY.test(text)),
256
330
  surfaceRoot: SURFACE_KEY.test(meta.replace(PARENT_OBJECT, "")),
257
- shell: SHELL_LAYOUT_IMPORT.test(source)
331
+ shell: false
258
332
  };
259
333
  }
260
334
 
335
+ // Whether a component draws the shell's top app bar, and with it Up: it, or a component it imports from the app,
336
+ // renders ShellLayout (directly or through the app's wrapper) or shell-web's ShellUpButton in a bar of its own.
337
+ function createShellBarReader(appFiles, read) {
338
+ const known = new Set(appFiles);
339
+ const verdicts = new Map();
340
+ async function drawsShellBar(file) {
341
+ if (verdicts.has(file)) return verdicts.get(file);
342
+ verdicts.set(file, false);
343
+ const source = await read(file);
344
+ let verdict = SHELL_LAYOUT_IMPORT.test(source) || SHELL_UP_TAG.test(splitVue(source).template?.text || "");
345
+ for (const match of source.matchAll(LOCAL_IMPORT)) {
346
+ if (verdict) break;
347
+ const imported = localComponent(file, match[1], known);
348
+ verdict = Boolean(imported) && await drawsShellBar(imported);
349
+ }
350
+ verdicts.set(file, verdict);
351
+ return verdict;
352
+ }
353
+ return drawsShellBar;
354
+ }
355
+
356
+ // The app's .vue file an import names: "@/components/Bar.vue" is src/components/Bar.vue; "./Bar" is beside the file.
357
+ function localComponent(file, specifier, known) {
358
+ const base = specifier.startsWith("@/")
359
+ ? path.posix.join("src", specifier.slice(2))
360
+ : path.posix.join(path.posix.dirname(file), specifier);
361
+ return [base, `${base}.vue`].find((candidate) => candidate.endsWith(".vue") && known.has(candidate)) || null;
362
+ }
363
+
261
364
  /**
262
365
  * The page rules: an inner page of a shell surface has a parent, its own or its layout's; a main-navigation
263
366
  * destination declares none.
@@ -274,6 +377,10 @@ async function checkPageParents({ root, appFiles, pageFiles, report }) {
274
377
  }
275
378
  const pages = new Map();
276
379
  for (const file of appFiles.filter(isPageFile)) pages.set(pageKey(file), readPage(file, await read(file)));
380
+ const drawsShellBar = createShellBarReader(appFiles, read);
381
+ for (const page of pages.values()) {
382
+ if (page.layout) page.shell = await drawsShellBar(page.file);
383
+ }
277
384
  const surfaces = readSurfaces(configSource, [...pages.values()].map(({ file }) => routeOf(file)));
278
385
  const scripts = appFiles.filter((file) => /\.(?:[cm]?[jt]s|vue)$/u.test(file));
279
386
  const navigation = readMainNavigation(await Promise.all(scripts.map(async (file) => blankJsComments(await read(file)))));