@jskit-ai/agent-docs 0.1.274 → 0.1.276
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/guide/agent/app-extras/assistant-conversation.md +7 -1
- package/guide/agent/app-extras/assistant-voice.md +5 -1
- package/guide/agent/app-setup/database-layer.md +30 -0
- package/guide/agent/app-setup/material-3-migration.md +180 -0
- package/guide/agent/app-setup/upgrading-jskit.md +5 -0
- package/guide/agent/framework/material-3.md +316 -73
- package/guide/agent/framework/typed-features.md +207 -0
- package/guide/agent/framework/ui-operations.md +18 -35
- package/guide/agent/framework/ui-pass.md +155 -0
- package/guide/agent/index.md +3 -0
- package/package.json +1 -1
- package/patterns/feature-package/example/booking-engine/package.json +1 -1
- package/patterns/minimal-foundation/example/package.json +5 -4
- package/patterns/minimal-foundation/example/src/main.js +5 -11
- package/patterns/minimal-foundation/example/src/pages/home/index.vue +5 -18
- package/patterns/minimal-foundation/example/src/theme/index.js +9 -0
- package/patterns/minimal-foundation/example/src/views/NotFound.vue +5 -4
- package/patterns/shell-foundation/example/package.json +6 -6
- package/patterns/shell-foundation/example/packages/main/package.json +1 -1
- package/patterns/shell-foundation/example/src/main.js +5 -11
- package/patterns/shell-foundation/example/src/pages/home/index.vue +21 -57
- package/patterns/shell-foundation/example/src/pages/home/settings/general/index.vue +24 -19
- package/patterns/shell-foundation/example/src/pages/home/settings.vue +27 -84
- package/patterns/shell-foundation/example/src/theme/index.js +9 -0
- package/patterns/shell-foundation/example/src/views/NotFound.vue +9 -7
- package/reference/autogen/PATTERN_INDEX.md +37 -37
- package/reference/autogen/README.md +1 -0
- package/reference/autogen/packages/agent-docs.md +8 -4
- package/reference/autogen/packages/assistant-core.md +1 -0
- package/reference/autogen/packages/console-web.md +5 -0
- package/reference/autogen/packages/http-web.md +29 -2
- package/reference/autogen/packages/kernel.md +2 -1
- package/reference/autogen/packages/material-web.md +54 -0
- package/reference/autogen/packages/shell-web.md +121 -9
- package/reference/autogen/tooling/jskit-catalog.md +81 -0
- package/reference/autogen/tooling/testUtils.md +7 -0
- package/skills/jskit/SKILL.md +17 -6
- package/skills/jskit/references/heuristics/INDEX.md +3 -3
- package/skills/jskit/references/heuristics/row-policies.md +10 -0
- package/skills/jskit/references/heuristics/typed-features.md +12 -0
- package/skills/jskit/references/heuristics/ui-contract.md +10 -7
- package/skills/jskit/references/heuristics/ui-testing.md +14 -8
- package/skills/jskit/references/material-3.md +315 -72
- package/skills/jskit/references/pattern-index.md +37 -37
- package/skills/jskit/references/patterns/app/minimal-foundation/example/package.json +5 -4
- package/skills/jskit/references/patterns/app/minimal-foundation/example/src/main.js +5 -11
- package/skills/jskit/references/patterns/app/minimal-foundation/example/src/pages/home/index.vue +5 -18
- package/skills/jskit/references/patterns/app/minimal-foundation/example/src/theme/index.js +9 -0
- package/skills/jskit/references/patterns/app/minimal-foundation/example/src/views/NotFound.vue +5 -4
- package/skills/jskit/references/patterns/app/shell-foundation/example/package.json +6 -6
- package/skills/jskit/references/patterns/app/shell-foundation/example/packages/main/package.json +1 -1
- package/skills/jskit/references/patterns/app/shell-foundation/example/src/main.js +5 -11
- package/skills/jskit/references/patterns/app/shell-foundation/example/src/pages/home/index.vue +21 -57
- package/skills/jskit/references/patterns/app/shell-foundation/example/src/pages/home/settings/general/index.vue +24 -19
- package/skills/jskit/references/patterns/app/shell-foundation/example/src/pages/home/settings.vue +27 -84
- package/skills/jskit/references/patterns/app/shell-foundation/example/src/theme/index.js +9 -0
- package/skills/jskit/references/patterns/app/shell-foundation/example/src/views/NotFound.vue +9 -7
- package/skills/jskit/references/patterns/assistant/assistant-surface/example/src/placement.js +1 -1
- package/skills/jskit/references/patterns/auth/accounts-hub/example/package.json +2 -2
- package/skills/jskit/references/patterns/auth/oidc-sign-in/example/package.json +2 -2
- package/skills/jskit/references/patterns/auth/supabase-auth/example/package.json +1 -1
- package/skills/jskit/references/patterns/connectors/calendar-cli/example/package.json +4 -4
- package/skills/jskit/references/patterns/console/console-surface/PATTERN.md +4 -2
- package/skills/jskit/references/patterns/console/console-surface/example/src/pages/console/index.vue +10 -40
- package/skills/jskit/references/patterns/console/console-surface/example/src/pages/console/settings/index.vue +3 -11
- package/skills/jskit/references/patterns/console/console-surface/example/src/pages/console/settings.vue +24 -72
- package/skills/jskit/references/patterns/console/console-surface/example/src/placement.js +28 -0
- package/skills/jskit/references/patterns/crud/crud-screen-set/PATTERN.md +13 -0
- package/skills/jskit/references/patterns/crud/crud-screen-set/example/books/BookEditPage.vue +2 -0
- package/skills/jskit/references/patterns/crud/crud-screen-set/example/books/BookFormFields.vue +0 -6
- package/skills/jskit/references/patterns/crud/crud-screen-set/example/books/BookListPage.vue +5 -2
- package/skills/jskit/references/patterns/crud/crud-screen-set/example/books/BookNewPage.vue +2 -0
- package/skills/jskit/references/patterns/crud/crud-screen-set/example/books/BookViewPage.vue +5 -9
- package/skills/jskit/references/patterns/crud/json-api-resource-package/example/packages/books/package.json +2 -2
- package/skills/jskit/references/patterns/database/mysql-application/example/package.json +1 -1
- package/skills/jskit/references/patterns/database/postgres-application/example/package.json +1 -1
- package/skills/jskit/references/patterns/realtime/realtime-application/example/package.json +2 -2
- package/skills/jskit/references/patterns/server/feature-package/example/booking-engine/package.json +1 -1
- package/skills/jskit/references/patterns/shell/application-shell/PATTERN.md +3 -1
- package/skills/jskit/references/patterns/shell/application-shell/example/expected-existing/src/pages/home/index.vue +5 -18
- package/skills/jskit/references/patterns/shell/application-shell/example/expected-existing/src/views/NotFound.vue +14 -0
- package/skills/jskit/references/patterns/shell/application-shell/example/src/pages/home/index.vue +21 -57
- package/skills/jskit/references/patterns/shell/application-shell/example/src/pages/home/settings/general/index.vue +24 -19
- package/skills/jskit/references/patterns/shell/application-shell/example/src/pages/home/settings.vue +27 -84
- package/skills/jskit/references/patterns/shell/application-shell/example/src/views/NotFound.vue +15 -0
- package/skills/jskit/references/patterns/ui/page-and-placement/example/src/components/SyncStatusElement.vue +2 -2
- package/skills/jskit/references/patterns/ui/page-and-placement/example/src/pages/home/reports/activity.vue +1 -1
- package/skills/jskit/references/patterns/ui/page-and-placement/example/src/pages/home/reports/overview.vue +1 -1
- package/skills/jskit/references/patterns/ui/page-and-placement/example/src/pages/home/reports.vue +6 -3
- package/skills/jskit/references/patterns/ui/page-and-placement/example/src/placement.js +1 -1
- package/skills/jskit/references/patterns/users/account-settings/example/src/components/account/settings/AccountSettingsNotificationsSection.vue +2 -8
- package/skills/jskit/references/patterns/users/account-settings/example/src/components/account/settings/AccountSettingsPreferencesSection.vue +2 -8
- package/skills/jskit/references/patterns/users/account-settings/example/src/components/account/settings/AccountSettingsProfileSection.vue +5 -11
- package/skills/jskit/references/patterns/users/account-settings/example/src/components/account/settings/AccountSettingsSignInMethodsSection.vue +3 -9
- package/skills/jskit/references/patterns/users/account-settings/example/src/pages/account/index.vue +7 -0
- package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users/package.json +2 -2
- package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users-workspace/package.json +3 -3
- package/skills/jskit/references/patterns/workspaces/workspace-surfaces/example/packages/main/src/client/components/AccountPendingInvitesCue.vue +1 -2
- package/skills/jskit/references/patterns/workspaces/workspace-surfaces/example/src/components/WorkspaceNotFoundCard.vue +5 -6
- package/skills/jskit/references/patterns/workspaces/workspace-surfaces/example/src/pages/admin/workspace/settings/index.vue +5 -12
- package/skills/jskit/references/patterns/workspaces/workspace-surfaces/example/src/pages/admin/workspace/settings.vue +24 -72
- package/skills/jskit/references/patterns/workspaces/workspace-surfaces/example/src/surfaces/admin/index.vue +3 -4
- package/skills/jskit/references/patterns/workspaces/workspace-surfaces/example/src/surfaces/app/index.vue +3 -3
- package/skills/jskit/references/typed-features.md +207 -0
- package/skills/jskit/references/ui-operations.md +18 -35
- package/skills/jskit/references/ui-pass.md +150 -0
- package/patterns/shell-foundation/example/src/pages/home/settings/index.vue +0 -7
- package/skills/jskit/references/patterns/app/shell-foundation/example/src/pages/home/settings/index.vue +0 -7
- package/skills/jskit/references/patterns/shell/application-shell/example/src/pages/home/settings/index.vue +0 -7
|
@@ -724,6 +724,107 @@ const rows = extractJsonRestCollectionRows(await api.resources.workspaceMembersh
|
|
|
724
724
|
}, createJsonRestContext(options.context)));
|
|
725
725
|
```
|
|
726
726
|
|
|
727
|
+
### One read per table
|
|
728
|
+
|
|
729
|
+
**A read that builds a list reads each table once for the whole list: one
|
|
730
|
+
query per table with an id-list filter, the rows grouped in memory. It never
|
|
731
|
+
reads one row, or runs one lookup, per item of the list.**
|
|
732
|
+
|
|
733
|
+
- Collect the ids from the first read, then read each related table once with
|
|
734
|
+
an id-list filter: a `searchSchema` entry
|
|
735
|
+
`{ type: "array", actualField: "<column>", filterOperator: "in" }`, read
|
|
736
|
+
through `queryAllJsonRestCollectionRows()`, or the resource's `include`.
|
|
737
|
+
Group the rows by id with `Map.groupBy()`.
|
|
738
|
+
- `getDocumentById()`, `queryDocuments()`, a service `get` or another owner's
|
|
739
|
+
single-record read inside a loop over the list is a read per row, however
|
|
740
|
+
the loop is spelled (`for`, `map` with `Promise.all`, a helper called per
|
|
741
|
+
item). So is a single-record helper reused for a list.
|
|
742
|
+
- Work derived from a row (hashing, preparing a form, assembling a snapshot)
|
|
743
|
+
runs once per distinct input in the call, not once per row it appears in.
|
|
744
|
+
- The list's query count follows its tables and pages (100 rows a page unless
|
|
745
|
+
the resource sets `queryMaxLimit`), never its rows: three hundred invoices
|
|
746
|
+
and their lines take a handful of queries, not three hundred more (see
|
|
747
|
+
[Budgets at realistic counts](#budgets-at-realistic-counts)).
|
|
748
|
+
|
|
749
|
+
Why: a read per row costs nothing with three test rows and seconds with a few
|
|
750
|
+
hundred real ones, and every lookup and visibility check inside it repeats too.
|
|
751
|
+
|
|
752
|
+
```js
|
|
753
|
+
// good: one read per table, however many invoices there are
|
|
754
|
+
const invoiceRows = await queryAllJsonRestCollectionRows(api.resources.invoices, {
|
|
755
|
+
queryParams: { filters: { workspace: workspaceId } },
|
|
756
|
+
transaction: resolveTransaction(options),
|
|
757
|
+
format: "plain"
|
|
758
|
+
}, options.context);
|
|
759
|
+
const lineRows = await queryAllJsonRestCollectionRows(api.resources.invoiceLines, {
|
|
760
|
+
queryParams: { filters: { invoiceIds: invoiceRows.map((invoice) => invoice.id) } },
|
|
761
|
+
transaction: resolveTransaction(options),
|
|
762
|
+
format: "plain"
|
|
763
|
+
}, options.context);
|
|
764
|
+
const linesByInvoice = Map.groupBy(lineRows, (line) => line.invoiceId);
|
|
765
|
+
return invoiceRows.map((invoice) => ({ ...invoice, lines: linesByInvoice.get(invoice.id) || [] }));
|
|
766
|
+
|
|
767
|
+
// bad: one more query, and its lookups and row policy, for every invoice
|
|
768
|
+
for (const invoice of invoiceRows) {
|
|
769
|
+
invoice.lines = await invoiceLines.queryDocuments({ invoiceId: invoice.id }, options);
|
|
770
|
+
}
|
|
771
|
+
```
|
|
772
|
+
|
|
773
|
+
### Bounded access checks
|
|
774
|
+
|
|
775
|
+
**A row policy costs what the rows its query reads cost, never what the
|
|
776
|
+
subject's stored history costs, and a write decides access once instead of
|
|
777
|
+
paying a read policy for every row it writes.**
|
|
778
|
+
|
|
779
|
+
- A `rowPolicy` adds one predicate to the query it is given: a join, `EXISTS`
|
|
780
|
+
or subquery correlated with the row being read, on indexed columns. It never
|
|
781
|
+
loads everything the subject was ever linked to (every source, notice or
|
|
782
|
+
grant in their history) to build an `IN` list, and never calls another
|
|
783
|
+
owner's access check once per item of that history. The
|
|
784
|
+
[row policies heuristic](https://github.com/mobily-enterprises/jskit-ai/blob/main/packages/agent-docs/heuristics/row-policies.md)
|
|
785
|
+
has the policy contract and its package composition.
|
|
786
|
+
- The policy runs on every single-record read, and a CRUD write includes one:
|
|
787
|
+
`createDocument()` and `patchDocumentById()` write with `returning: "full"`
|
|
788
|
+
and read the record back through the policy, and `patch` and `delete` first
|
|
789
|
+
look the record up through it. A write therefore pays the read policy for
|
|
790
|
+
each row it writes.
|
|
791
|
+
- A write path decides access once, inside its transaction. A batch write (a
|
|
792
|
+
delivery, an import, a fan-out) whose per-row read-back would pay an
|
|
793
|
+
expensive policy writes through a repository method added with
|
|
794
|
+
`decorateRepository`, on `resolveRepoClient(database.knex, options)`,
|
|
795
|
+
setting the same columns the resource would, its ownership columns
|
|
796
|
+
included, and reading back only what it returns.
|
|
797
|
+
- Its test runs one read and one write for a subject with a short history and
|
|
798
|
+
with a realistic long one, and asserts the same query count.
|
|
799
|
+
|
|
800
|
+
Why: a check sized by history grows with every record the subject receives,
|
|
801
|
+
so the same write that takes milliseconds in a test takes seconds a year later.
|
|
802
|
+
|
|
803
|
+
```js
|
|
804
|
+
// good: one predicate on the row being read
|
|
805
|
+
function noticeVisibilityPolicy({ query, context, column, value, db }) {
|
|
806
|
+
const userId = resolveActorId(context);
|
|
807
|
+
if (!userId) return false;
|
|
808
|
+
query.where(column("recipientId"), value("recipientId", userId)).whereExists(
|
|
809
|
+
db("sources").select(1)
|
|
810
|
+
.whereColumn("sources.id", column("sourceId"))
|
|
811
|
+
.where("sources.status", "published")
|
|
812
|
+
);
|
|
813
|
+
return true;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
// bad: every source the recipient was ever notified about, checked one by one
|
|
817
|
+
async function noticeVisibilityPolicy({ query, context, column, value }) {
|
|
818
|
+
const sourceIds = await noticeHistory.listSourceIds(resolveActorId(context));
|
|
819
|
+
const readable = [];
|
|
820
|
+
for (const sourceId of sourceIds) {
|
|
821
|
+
if (await sources.canRead(sourceId, context)) readable.push(sourceId);
|
|
822
|
+
}
|
|
823
|
+
query.whereIn(column("sourceId"), readable.map((id) => value("sourceId", id)));
|
|
824
|
+
return true;
|
|
825
|
+
}
|
|
826
|
+
```
|
|
827
|
+
|
|
727
828
|
### Routes
|
|
728
829
|
|
|
729
830
|
**A route does HTTP work only (status, cookies, redirects) and runs an action
|
|
@@ -910,6 +1011,112 @@ export function resetInvoiceCacheForTests() {
|
|
|
910
1011
|
}
|
|
911
1012
|
```
|
|
912
1013
|
|
|
1014
|
+
### Budgets at realistic counts
|
|
1015
|
+
|
|
1016
|
+
**A list, batch or write path has a database test at the row counts and
|
|
1017
|
+
history a real workspace holds, and that test fails above a fixed query
|
|
1018
|
+
budget, one that a read per row or a history-sized check cannot meet.**
|
|
1019
|
+
|
|
1020
|
+
- Seed hundreds of records and long per-person histories, not one to three
|
|
1021
|
+
rows: a read per row and a history-sized policy cost nothing at three.
|
|
1022
|
+
- Count the queries with the test Knex instance's `query` event and assert a
|
|
1023
|
+
ceiling: the count the correct implementation needs at that size, which a
|
|
1024
|
+
read per row overshoots by hundreds. A path that pages nothing (one read,
|
|
1025
|
+
one write) asserts the same count at a short and a long history. A query
|
|
1026
|
+
count is stable in CI; also report the time the call took.
|
|
1027
|
+
- Meet the budget by fixing the reads ([One read per
|
|
1028
|
+
table](#one-read-per-table), [Bounded access checks](#bounded-access-checks)),
|
|
1029
|
+
never by shrinking the data or raising the budget.
|
|
1030
|
+
|
|
1031
|
+
Why: without a budget at real counts, a path that does one read per row ships,
|
|
1032
|
+
and its first sign is a request that takes seconds in production.
|
|
1033
|
+
|
|
1034
|
+
```js
|
|
1035
|
+
// good
|
|
1036
|
+
test("the invoice list reads each table once", async () => {
|
|
1037
|
+
await seedInvoices(knex, { workspace, count: 300, linesEach: 5 });
|
|
1038
|
+
let queries = 0;
|
|
1039
|
+
const count = () => { queries += 1; };
|
|
1040
|
+
knex.on("query", count);
|
|
1041
|
+
try {
|
|
1042
|
+
await invoices.listWithLines(workspace, { context });
|
|
1043
|
+
} finally {
|
|
1044
|
+
knex.off("query", count);
|
|
1045
|
+
}
|
|
1046
|
+
// A page of each table at a time; one read per invoice would add 300.
|
|
1047
|
+
assert.ok(queries <= 40, `${queries} queries`);
|
|
1048
|
+
});
|
|
1049
|
+
|
|
1050
|
+
// bad: three invoices and no budget, so a read per invoice passes
|
|
1051
|
+
test("the invoice list has its lines", async () => {
|
|
1052
|
+
await seedInvoices(knex, { workspace, count: 3, linesEach: 1 });
|
|
1053
|
+
assert.equal((await invoices.listWithLines(workspace, { context })).length, 3);
|
|
1054
|
+
});
|
|
1055
|
+
```
|
|
1056
|
+
|
|
1057
|
+
### Time limits are not fixes
|
|
1058
|
+
|
|
1059
|
+
**A slow test, suite, preparation or request is a defect to find, never a time
|
|
1060
|
+
limit to raise. Measure where the time goes, fix that cause, and keep the
|
|
1061
|
+
default limits.**
|
|
1062
|
+
|
|
1063
|
+
- Raising `--test-timeout`, a test's `{ timeout }`, Playwright's `timeout`,
|
|
1064
|
+
`test.setTimeout()` or an `expect` timeout, adding retries, or calling a
|
|
1065
|
+
request in a loop until it answers hides the cause and lets it grow.
|
|
1066
|
+
- Read the preparation phases' durations and the query counts first; the
|
|
1067
|
+
cause is usually a read per row, a history-sized policy, or a database
|
|
1068
|
+
prepared by replaying migrations ([Test databases are prepared
|
|
1069
|
+
once](#test-databases-are-prepared-once)).
|
|
1070
|
+
- A longer limit is right only for work that is long by nature, such as a
|
|
1071
|
+
fixture of deliberate size, and the test names that work.
|
|
1072
|
+
- A time limit still bounds every run, so a hang ends; it is not a speed
|
|
1073
|
+
setting.
|
|
1074
|
+
|
|
1075
|
+
Why: a raised limit absorbs today's slowness and every later one, until a run
|
|
1076
|
+
takes hours and nobody knows which change made it slow.
|
|
1077
|
+
|
|
1078
|
+
```js
|
|
1079
|
+
// good: the slow step was found and fixed, so the default limit holds
|
|
1080
|
+
test("delivers a batch of notices", async () => {
|
|
1081
|
+
await deliveries.processBatch(workspace, { context });
|
|
1082
|
+
});
|
|
1083
|
+
|
|
1084
|
+
// bad
|
|
1085
|
+
test("delivers a batch of notices", { timeout: 600_000 }, async () => {
|
|
1086
|
+
await deliveries.processBatch(workspace, { context });
|
|
1087
|
+
});
|
|
1088
|
+
```
|
|
1089
|
+
|
|
1090
|
+
### Test databases are prepared once
|
|
1091
|
+
|
|
1092
|
+
**A test run prepares each database once, from a stored snapshot of its
|
|
1093
|
+
schema, migration ledger and seed rows checked by a digest of their sources,
|
|
1094
|
+
and never replays the whole migration history inside the run.**
|
|
1095
|
+
|
|
1096
|
+
- The snapshot is rebuilt only when the digest of the migration and seed
|
|
1097
|
+
sources changes, once and outside the timed tests; every disposable
|
|
1098
|
+
database, and every database a test migrated up or down, is restored from
|
|
1099
|
+
it.
|
|
1100
|
+
- The full migration history runs once, in its own fresh-schema check
|
|
1101
|
+
([database verification](https://mobily-enterprises.github.io/jskit-ai/guide/app-setup/database-layer#verification)).
|
|
1102
|
+
- The harness reports each preparation phase and its duration, and the suite
|
|
1103
|
+
has a time budget, so slow preparation shows instead of hiding inside the
|
|
1104
|
+
next test that waits for it.
|
|
1105
|
+
- The database layer guide's [integration test
|
|
1106
|
+
databases](https://mobily-enterprises.github.io/jskit-ai/guide/app-setup/database-layer#integration-test-databases)
|
|
1107
|
+
gives the snapshot's contents per engine.
|
|
1108
|
+
|
|
1109
|
+
Why: the migration history only grows, so a run that replays it whenever a
|
|
1110
|
+
schema changed gets slower with every migration the application adds.
|
|
1111
|
+
|
|
1112
|
+
```js
|
|
1113
|
+
// good: the application's test support restores the snapshot its digest names
|
|
1114
|
+
before(async () => restoreTestDatabase(knex, await testDatabaseSnapshot()));
|
|
1115
|
+
|
|
1116
|
+
// bad: every file replays every migration
|
|
1117
|
+
before(() => knex.migrate.latest());
|
|
1118
|
+
```
|
|
1119
|
+
|
|
913
1120
|
## Files and layout
|
|
914
1121
|
|
|
915
1122
|
### Size limits
|
|
@@ -23,37 +23,17 @@ only when defining or diagnosing topology. Resolve current dynamic parameters
|
|
|
23
23
|
to an absolute URL or route object for sibling/child links—never bind a route
|
|
24
24
|
template or relative string raw to Vue Router `to`.
|
|
25
25
|
|
|
26
|
-
##
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
[`router.back()`](https://router.vuejs.org/guide/essentials/navigation.html#traverse-history));
|
|
38
|
-
`usePaths().page()` from `@jskit-ai/shell-web/client/navigation/usePaths`
|
|
39
|
-
resolves a destination, not history. Do not invent a second navigation stack.
|
|
40
|
-
|
|
41
|
-
For direct links, new tabs, or no usable in-app history, use a deliberate safe
|
|
42
|
-
fallback resolved through the existing surface/workspace-aware path helpers.
|
|
43
|
-
Do not infer a usable previous page from `window.history.length` alone or send
|
|
44
|
-
people outside the app unexpectedly. Keep existing unsaved-change guards.
|
|
45
|
-
|
|
46
|
-
A fixed parent/list destination is valid when the task calls for it; name it
|
|
47
|
-
explicitly, for example **Back to appointments**. It must not silently replace
|
|
48
|
-
ordinary Back. Shared CRUD `listLocation`/`listUrlTemplate` links are fixed
|
|
49
|
-
destinations, not proof of previous-page behavior. Keep Back distinct from
|
|
50
|
-
Cancel, Close, and a wizard's Previous step.
|
|
51
|
-
|
|
52
|
-
Place Back consistently near the start of the page's existing action region,
|
|
53
|
-
before forward/primary actions in reading and keyboard order. Reuse shell or
|
|
54
|
-
shared-screen controls; avoid duplicates, drawer-only access, or adding a page
|
|
55
|
-
heading just to house it. Keep its meaning and accessible name clear at every
|
|
56
|
-
supported width.
|
|
26
|
+
## Navigation and Up
|
|
27
|
+
|
|
28
|
+
Follow [Navigation and Up](./material-3.md#navigation-and-up), a MUST that
|
|
29
|
+
plain `jskit check` enforces: the shell owns the Up arrow (one, in the top app
|
|
30
|
+
bar, on every inner page) and the per-tab trail. An app declares each inner
|
|
31
|
+
page's parent as route meta `jskit.parent`, resolves every inner link through
|
|
32
|
+
JSKIT navigation, finishes custom forms with `useShellNavigation().finish()`,
|
|
33
|
+
and never writes back logic, `router.back()`, `history.back()`,
|
|
34
|
+
`window.location` navigation or a "Back to X" control. Keep Up distinct from
|
|
35
|
+
Cancel, Close, and a wizard's Previous step; keep existing unsaved-change
|
|
36
|
+
guards.
|
|
57
37
|
|
|
58
38
|
`createShellRouter()` owns scroll position (`shell/application-shell`
|
|
59
39
|
pattern); never reset scroll in page components.
|
|
@@ -95,10 +75,13 @@ pattern, never its private CSS.
|
|
|
95
75
|
Exercise user-facing changes with Playwright at compact, medium, and expanded
|
|
96
76
|
widths. Check overflow, clipped text, duplicate navigation, route placement,
|
|
97
77
|
actions, skeleton replacement, error feedback, and target sizes. Use relative
|
|
98
|
-
URLs. With `PLAYWRIGHT_BASE_URL` set, start no duplicate server. For
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
78
|
+
URLs. With `PLAYWRIGHT_BASE_URL` set, start no duplicate server. For Up
|
|
79
|
+
(`shellBackButton(page)` and `pressShellBack(page)` from
|
|
80
|
+
`@jskit-ai/shell-web/test/shellNavigation`), reach a page from two origins (one
|
|
81
|
+
sideways) and verify each Up returns to its own origin and that chains unwind;
|
|
82
|
+
open the page directly and verify Up reaches its declared parent; verify
|
|
83
|
+
main-menu destinations show no arrow, a reload keeps the trail, and Save or
|
|
84
|
+
Cancel never leaves the form in it.
|
|
102
85
|
|
|
103
86
|
A managed host owns its browser and may provide
|
|
104
87
|
`PLAYWRIGHT_STORAGE_STATE`; never print or commit that value, bypass it,
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
<!-- Generated by `npm run agent-docs:build` from `packages/agent-docs/site/guide/framework/ui-pass.md`. Do not edit manually. -->
|
|
2
|
+
|
|
3
|
+
---
|
|
4
|
+
title: UI pass
|
|
5
|
+
description: Make a task's changes, a section of the app, or the whole app exact Material 3 without changing behavior.
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# UI pass
|
|
9
|
+
|
|
10
|
+
A UI pass makes JSKIT UI exact Material 3 at one of three scopes: **the
|
|
11
|
+
changes** a task made (the default), **a section** of the app, or **the whole
|
|
12
|
+
app**. Read the complete [Material 3 rules](./material-3.md) first; navigation
|
|
13
|
+
and Up come first there.
|
|
14
|
+
|
|
15
|
+
## Every scope
|
|
16
|
+
|
|
17
|
+
- **The same rules:** the [Material 3 rules](./material-3.md), their exact sizes,
|
|
18
|
+
`jskit check`, `jskit check m3`, and screenshots at 390, 768 and 1280 wide, in
|
|
19
|
+
light and dark.
|
|
20
|
+
- **Navigation is must-fix at every scope:** fix every `nav/*` error inside the
|
|
21
|
+
scope (declare parents, resolve links through JSKIT, delete back controls and
|
|
22
|
+
back logic); never only report one. Plain `jskit check` must not end with a
|
|
23
|
+
new error. Errors outside the scope, in an app not yet converted, are
|
|
24
|
+
reported as must-fix findings.
|
|
25
|
+
- **Behavior otherwise stays exactly as it is:** no feature, field, action,
|
|
26
|
+
permission, validation or data change. A defect, missing behavior or test gap
|
|
27
|
+
you notice is a finding for the report, not a fix.
|
|
28
|
+
- **Exceptions:** take the [approved routes](./material-3.md#exceptions) first.
|
|
29
|
+
Add an `m3-check: allow` exception only after the person agreed to it, and
|
|
30
|
+
list every exception the changes add, with its file, line and reason, in the
|
|
31
|
+
report.
|
|
32
|
+
- **JSKIT first:** if JSKIT itself blocks an exact result, report the exact JSKIT
|
|
33
|
+
change (package, file, what is missing). Never compensate with app CSS,
|
|
34
|
+
`!important` or a copied package file.
|
|
35
|
+
- **Screenshots** come from the managed browser or a temporary Playwright spec.
|
|
36
|
+
Keep them out of the repository (`test-results/` is ignored) and delete the
|
|
37
|
+
temporary spec afterwards. With `defaultTheme: "system"`, `emulateMedia`
|
|
38
|
+
selects the scheme:
|
|
39
|
+
|
|
40
|
+
```js
|
|
41
|
+
import { test } from "@playwright/test";
|
|
42
|
+
|
|
43
|
+
for (const scheme of ["light", "dark"]) {
|
|
44
|
+
for (const width of [390, 768, 1280]) {
|
|
45
|
+
test(`suppliers ${width} ${scheme}`, async ({ page }) => {
|
|
46
|
+
await page.emulateMedia({ colorScheme: scheme });
|
|
47
|
+
await page.setViewportSize({ width, height: 900 });
|
|
48
|
+
await page.goto("/home/suppliers");
|
|
49
|
+
await page.screenshot({ path: `test-results/ui-pass/after/suppliers-${width}-${scheme}.png`, fullPage: true });
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
- **Look at every screenshot** before reporting. They are the only proof of
|
|
56
|
+
what the checks cannot see: layout, spacing, contrast, overflow, clipped
|
|
57
|
+
labels, borders where tones belong, colour that only works in one scheme, an
|
|
58
|
+
Up arrow where it should not be or missing where it should.
|
|
59
|
+
- **Prove Up** on the screens in scope, with `shellBackButton(page)` and
|
|
60
|
+
`pressShellBack(page)` from `@jskit-ai/shell-web/test/shellNavigation`:
|
|
61
|
+
contact → supplier → Up shows the contact; a supplier opened from a pasted
|
|
62
|
+
link → Up shows the Suppliers list; no arrow on main-menu pages; no "Back to
|
|
63
|
+
X" anywhere.
|
|
64
|
+
|
|
65
|
+
## 1. The changes
|
|
66
|
+
|
|
67
|
+
The default, and what an automatic UI pass after a plan runs.
|
|
68
|
+
|
|
69
|
+
- **In scope:** every screen, component and style the task created or edited,
|
|
70
|
+
plus the screens that render them. Explicit commits replace "the task's own
|
|
71
|
+
changes" when they are named.
|
|
72
|
+
- **Method:**
|
|
73
|
+
1. List the changed UI files and the screens that render them.
|
|
74
|
+
2. Check each against the Material 3 rules and fix what is off, inside the
|
|
75
|
+
changes only.
|
|
76
|
+
3. Run `jskit check m3` on the changed UI files until it reports nothing,
|
|
77
|
+
and plain `jskit check` until it passes.
|
|
78
|
+
4. Take after screenshots of each affected screen, and look at them.
|
|
79
|
+
5. Run the focused tests of the affected screens.
|
|
80
|
+
- **Report:** what was fixed; anything outside the changes that looks wrong, as
|
|
81
|
+
findings only (not fixed; navigation ones marked must-fix); the check
|
|
82
|
+
results; the screenshots; what is unverified.
|
|
83
|
+
|
|
84
|
+
Example: a task added a Delete button and a "Back to suppliers" link to the
|
|
85
|
+
supplier page. The pass replaces the button's `color="#d32f2f"` and
|
|
86
|
+
`variant="outlined"` with the error colour role, removes the link, declares the
|
|
87
|
+
page's parent (`/suppliers`), checks `src/pages/home/suppliers/[supplierId].vue`
|
|
88
|
+
clean, and shows the supplier at the six sizes. It reports, unfixed, that the
|
|
89
|
+
Contacts list scrolls sideways at 390.
|
|
90
|
+
|
|
91
|
+
## 2. A section
|
|
92
|
+
|
|
93
|
+
On request ("UI pass the Suppliers section").
|
|
94
|
+
|
|
95
|
+
- **In scope:** a named area: a module, a route subtree, a set of screens or a
|
|
96
|
+
shared component family. Every screen in it, touched or not, so the section is
|
|
97
|
+
consistent.
|
|
98
|
+
- **Method:**
|
|
99
|
+
1. Inventory the section's screens and the components they render.
|
|
100
|
+
2. Take before screenshots of each screen, with its empty, error and dialog
|
|
101
|
+
states where they exist.
|
|
102
|
+
3. Map each screen pattern to its Material 3 canonical layout (list-detail,
|
|
103
|
+
supporting pane, feed) and components, in a table.
|
|
104
|
+
4. Convert, navigation first: declare each inner page's parent, route every
|
|
105
|
+
inner link through JSKIT navigation, delete back handlers and "Back to X"
|
|
106
|
+
controls.
|
|
107
|
+
5. Take after screenshots beside the before ones.
|
|
108
|
+
6. Run `jskit check m3` on the section until it reports nothing.
|
|
109
|
+
7. Delete the app CSS that is no longer needed.
|
|
110
|
+
8. Change browser tests only where markup changed; every behavior assertion
|
|
111
|
+
stays.
|
|
112
|
+
- **Report:** a before/after gallery, the mapping, the non-blank CSS lines
|
|
113
|
+
(`<style>` blocks and CSS files) before and after, the check results, the
|
|
114
|
+
Up proof and the test results.
|
|
115
|
+
|
|
116
|
+
Example: the Suppliers section has a list, a record, a new and an edit page.
|
|
117
|
+
The mapping reads: list as a list with filter chips and a FAB, list-detail on
|
|
118
|
+
expanded widths; record as a header with status chips and value-first lists;
|
|
119
|
+
new and edit as a form with Cancel and Save in the bar. The pass removes a
|
|
120
|
+
bordered filter panel and 46 lines of CSS, declares the three inner pages'
|
|
121
|
+
parents, and the check reports nothing for `src/pages/home/suppliers`.
|
|
122
|
+
|
|
123
|
+
## 3. The whole app
|
|
124
|
+
|
|
125
|
+
On request ("UI pass the whole app"). Every screen, the shell and navigation
|
|
126
|
+
included.
|
|
127
|
+
|
|
128
|
+
1. **Inventory and before screenshots** of every screen type: sign-in, home,
|
|
129
|
+
dashboard, every list, one record per module, every form type, settings, a
|
|
130
|
+
dialog, an empty state and an error state.
|
|
131
|
+
2. **A short spec:** the foundation decisions (seed colour, scheme generation,
|
|
132
|
+
type, shape, icons, Vuetify defaults) and a table mapping every screen
|
|
133
|
+
pattern to its Material 3 layout and components.
|
|
134
|
+
3. **Foundation first:** the theme from one seed, type, shape, icons and Vuetify
|
|
135
|
+
defaults, then the app shell and navigation (every inner page's parent, so
|
|
136
|
+
plain `jskit check` passes).
|
|
137
|
+
4. **Screens in packages,** at most three at a time, never two on the same file.
|
|
138
|
+
5. **After screenshots** for each package.
|
|
139
|
+
6. **Delete as you go:** app CSS shrinks to almost nothing, with no hex colour
|
|
140
|
+
outside `src/theme/`; `jskit check m3` reports nothing for the whole app.
|
|
141
|
+
7. **Tests:** browser tests change only where markup changed, every behavior
|
|
142
|
+
assertion stays the same, and nothing regresses.
|
|
143
|
+
8. **An independent review:** a fresh agent, given only the screenshots and the
|
|
144
|
+
spec, lists every departure from Material 3 as MUST or SHOULD. Fix every MUST
|
|
145
|
+
and review again until there are none.
|
|
146
|
+
|
|
147
|
+
- **Report:** the gallery, the reviewer's final pass with no MUST items, the
|
|
148
|
+
counts (screens, CSS lines before and after, check findings before and
|
|
149
|
+
after), the Up proof and the test results.
|
|
150
|
+
|
|
151
|
+
Example: a twelve-screen app. The spec picks the existing brand orange as seed
|
|
152
|
+
and maps four lists, four records, three forms and settings. The foundation
|
|
153
|
+
package replaces a hand-picked palette and MDI icons; three screen packages
|
|
154
|
+
follow. App CSS goes from 640 lines to 38, the check from 212 findings to none,
|
|
155
|
+
and the second review has no MUST items.
|
package/guide/agent/index.md
CHANGED
|
@@ -11,6 +11,7 @@ apply or adapt. It does not prescribe an agent host or project orchestrator.
|
|
|
11
11
|
- [Quickstart](/guide/app-setup/quickstart)
|
|
12
12
|
- [Application foundations](/guide/app-setup/initial-scaffolding)
|
|
13
13
|
- [Upgrade JSKIT](/guide/app-setup/upgrading-jskit)
|
|
14
|
+
- [Move an app to the Material 3 defaults](/guide/app-setup/material-3-migration)
|
|
14
15
|
- [Migrate an existing application](/guide/app-setup/existing-application-migration)
|
|
15
16
|
- [A more interesting shell](/guide/app-setup/a-more-interesting-shell)
|
|
16
17
|
- [Authentication](/guide/app-setup/authentication)
|
|
@@ -32,6 +33,8 @@ apply or adapt. It does not prescribe an agent host or project orchestrator.
|
|
|
32
33
|
composable, component, client factory or client provider
|
|
33
34
|
- [UI operations](/guide/framework/ui-operations)
|
|
34
35
|
- [Material 3](/guide/framework/material-3)
|
|
36
|
+
- [UI pass](/guide/framework/ui-pass): make a task's changes, a section or the
|
|
37
|
+
whole app exact Material 3
|
|
35
38
|
- [Translating a JSKIT app](/guide/framework/translation)
|
|
36
39
|
- [Complete source pattern library](/patterns/)
|
|
37
40
|
|
package/package.json
CHANGED
|
@@ -34,7 +34,8 @@
|
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"@local/main": "0.1.0",
|
|
36
36
|
"@fastify/static": "^10.1.3",
|
|
37
|
-
"@jskit-ai/kernel": "0.2.
|
|
37
|
+
"@jskit-ai/kernel": "0.2.10",
|
|
38
|
+
"@jskit-ai/material-web": "0.1.0",
|
|
38
39
|
"@tanstack/vue-query": "^5.101.0",
|
|
39
40
|
"fastify": "^5.12.7",
|
|
40
41
|
"pino": "^10.3.1",
|
|
@@ -42,14 +43,14 @@
|
|
|
42
43
|
"pinia": "^4.0.3",
|
|
43
44
|
"vue": "^3.5.44",
|
|
44
45
|
"vue-router": "^5.3.1",
|
|
45
|
-
"vuetify": "^4.
|
|
46
|
-
"@jskit-ai/http-runtime": "0.2.
|
|
46
|
+
"vuetify": "^4.3.0",
|
|
47
|
+
"@jskit-ai/http-runtime": "0.2.10",
|
|
47
48
|
"@fastify/fast-json-stringify-compiler": "^5.1.0",
|
|
48
49
|
"@vue/devtools-api": "^8.2.1"
|
|
49
50
|
},
|
|
50
51
|
"devDependencies": {
|
|
51
52
|
"@jskit-ai/config-eslint": "0.1.245",
|
|
52
|
-
"@jskit-ai/jskit-catalog": "0.2.
|
|
53
|
+
"@jskit-ai/jskit-catalog": "0.2.28",
|
|
53
54
|
"@playwright/test": "1.61.1",
|
|
54
55
|
"@vitejs/plugin-vue": "^6.0.7",
|
|
55
56
|
"eslint": "^10.8.0",
|
|
@@ -4,9 +4,11 @@ import { QueryClient, VueQueryPlugin } from "@tanstack/vue-query";
|
|
|
4
4
|
import { createRouter, createWebHistory } from "vue-router/auto";
|
|
5
5
|
import { routes } from "vue-router/auto-routes";
|
|
6
6
|
import "vuetify/styles";
|
|
7
|
+
import "@jskit-ai/material-web/client/material.css";
|
|
7
8
|
import { createVuetify } from "vuetify";
|
|
8
|
-
import {
|
|
9
|
+
import { createMaterialVuetifyOptions } from "@jskit-ai/material-web/client";
|
|
9
10
|
import App from "./App.vue";
|
|
11
|
+
import { theme } from "./theme/index.js";
|
|
10
12
|
import NotFoundView from "./views/NotFound.vue";
|
|
11
13
|
import { bootInstalledClientModules } from "virtual:jskit-client-bootstrap";
|
|
12
14
|
import { createSurfaceRuntime } from "@jskit-ai/kernel/shared/surface/runtime";
|
|
@@ -39,16 +41,8 @@ const { router, fallbackRoute } = createShellRouter({
|
|
|
39
41
|
}
|
|
40
42
|
});
|
|
41
43
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
defaultTheme: "light"
|
|
45
|
-
},
|
|
46
|
-
icons: {
|
|
47
|
-
defaultSet: "mdi",
|
|
48
|
-
aliases: mdiAliases,
|
|
49
|
-
sets: { mdi }
|
|
50
|
-
}
|
|
51
|
-
});
|
|
44
|
+
// Material 3 from the app's theme (src/theme/index.js), with Material Symbols behind every icon alias.
|
|
45
|
+
const vuetify = createVuetify(createMaterialVuetifyOptions(theme));
|
|
52
46
|
const pinia = createPinia();
|
|
53
47
|
const queryClient = new QueryClient({
|
|
54
48
|
defaultOptions: {
|
|
@@ -1,21 +1,8 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<section class="
|
|
3
|
-
<v-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
</v-sheet>
|
|
2
|
+
<section class="d-flex flex-column align-center ga-4 py-16 text-center on-surface-variant">
|
|
3
|
+
<v-icon icon="$inbox" size="48" />
|
|
4
|
+
<p class="text-body-large mb-0">
|
|
5
|
+
Recent work, saved records, and next actions will appear here once this app has data.
|
|
6
|
+
</p>
|
|
8
7
|
</section>
|
|
9
8
|
</template>
|
|
10
|
-
|
|
11
|
-
<style scoped>
|
|
12
|
-
.home-start-screen {
|
|
13
|
-
--home-start-panel-padding: 1rem;
|
|
14
|
-
margin-inline: auto;
|
|
15
|
-
max-width: 48rem;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
.home-start-screen__panel {
|
|
19
|
-
padding: var(--home-start-panel-padding);
|
|
20
|
-
}
|
|
21
|
-
</style>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// The app's only theme facts. Material 3's light and dark colour schemes are generated from this one seed colour and
|
|
2
|
+
// follow the device's setting: change the seed to rebrand. A colour the app really owns goes in `colors` by camelCase
|
|
3
|
+
// name (it gets its own container roles, harmonized to the seed), and an icon of its own goes in `icons` as
|
|
4
|
+
// `{ name: "<24x24 SVG path>" }`, used as `$name`.
|
|
5
|
+
export const theme = {
|
|
6
|
+
seed: "#6750A4",
|
|
7
|
+
colors: {},
|
|
8
|
+
icons: {}
|
|
9
|
+
};
|
|
@@ -5,9 +5,10 @@ const message = "The page you requested does not exist.";
|
|
|
5
5
|
|
|
6
6
|
<template>
|
|
7
7
|
<v-container class="fill-height d-flex align-center justify-center">
|
|
8
|
-
<
|
|
9
|
-
<v-
|
|
10
|
-
<
|
|
11
|
-
|
|
8
|
+
<section class="on-surface-variant text-center">
|
|
9
|
+
<v-icon icon="$searchOff" size="48" />
|
|
10
|
+
<h1 class="text-headline-small on-surface mt-4 mb-2">{{ title }}</h1>
|
|
11
|
+
<p class="text-body-large mb-0">{{ message }}</p>
|
|
12
|
+
</section>
|
|
12
13
|
</v-container>
|
|
13
14
|
</template>
|
|
@@ -34,7 +34,8 @@
|
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"@local/main": "0.1.0",
|
|
36
36
|
"@fastify/static": "^10.1.3",
|
|
37
|
-
"@jskit-ai/kernel": "0.2.
|
|
37
|
+
"@jskit-ai/kernel": "0.2.10",
|
|
38
|
+
"@jskit-ai/material-web": "0.1.0",
|
|
38
39
|
"@tanstack/vue-query": "^5.101.0",
|
|
39
40
|
"fastify": "^5.12.7",
|
|
40
41
|
"pino": "^10.3.1",
|
|
@@ -42,16 +43,15 @@
|
|
|
42
43
|
"pinia": "^4.0.3",
|
|
43
44
|
"vue": "^3.5.44",
|
|
44
45
|
"vue-router": "^5.3.1",
|
|
45
|
-
"vuetify": "^4.
|
|
46
|
-
"@jskit-ai/http-runtime": "0.2.
|
|
47
|
-
"@
|
|
48
|
-
"@jskit-ai/shell-web": "0.2.10",
|
|
46
|
+
"vuetify": "^4.3.0",
|
|
47
|
+
"@jskit-ai/http-runtime": "0.2.10",
|
|
48
|
+
"@jskit-ai/shell-web": "0.2.11",
|
|
49
49
|
"@fastify/fast-json-stringify-compiler": "^5.1.0",
|
|
50
50
|
"@vue/devtools-api": "^8.2.1"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"@jskit-ai/config-eslint": "0.1.245",
|
|
54
|
-
"@jskit-ai/jskit-catalog": "0.2.
|
|
54
|
+
"@jskit-ai/jskit-catalog": "0.2.28",
|
|
55
55
|
"@playwright/test": "1.61.1",
|
|
56
56
|
"@vitejs/plugin-vue": "^6.0.7",
|
|
57
57
|
"eslint": "^10.8.0",
|