@abgov/nx-adsp 13.18.0-beta.7 → 13.18.0-beta.9
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/package.json +1 -1
- package/src/generators/vue-app/files/AGENTS.md__tmpl__ +20 -3
- package/src/generators/vue-app/files/src/App.vue__tmpl__ +25 -3
- package/src/generators/vue-app/vue-app.spec.ts +14 -2
- package/src/generators/vue-components/files/AGENTS.md__tmpl__ +20 -3
- package/src/generators/vue-components/files/src/lib/patterns/AppLayout.vue__tmpl__ +9 -22
- package/src/generators/vue-components/files/src/lib/patterns/AppSideMenu.vue__tmpl__ +23 -0
- package/src/generators/vue-components/vue-components.spec.ts +13 -0
package/package.json
CHANGED
|
@@ -172,8 +172,8 @@ from **`<%= goaImportPath %>`**, not hand-rolled markup:
|
|
|
172
172
|
|
|
173
173
|
| Component | Purpose |
|
|
174
174
|
|---|---|
|
|
175
|
-
| `AppSideMenu` | `goa-work-side-menu`. Takes `heading`/`primaryItems`/`secondaryItems`/`accountItems` props (each item: `{ label, to?, icon?, badge?, current? }`) and emits `itemClick` — it doesn't know about routing or auth, so `App.vue` computes `current`/handles the click itself. `App.vue` currently only populates `accountItems` (a single Sign in/out item); add real nav items to `primaryItems`/`secondaryItems` as routes are added |
|
|
176
|
-
| `AppLayout` | The content gutter every view renders inside (see the key files table)
|
|
175
|
+
| `AppSideMenu` | `goa-work-side-menu`. Takes `heading`/`primaryItems`/`secondaryItems`/`accountItems` props (each item: `{ label, to?, icon?, badge?, current? }`) and emits `itemClick` — it doesn't know about routing or auth, so `App.vue` computes `current`/handles the click itself. `App.vue` currently only populates `accountItems` (a single Sign in/out item); add real nav items to `primaryItems`/`secondaryItems` as routes are added. Also owns the skip-to-main-content landmark for this layout — `AppLayout` deliberately doesn't duplicate it. Also takes an optional `#topbar` slot — a slim row above the routed content, for something like a notification bell; only renders when given content, unused by default |
|
|
176
|
+
| `AppLayout` | The content gutter every view renders inside (see the key files table) |
|
|
177
177
|
| `SessionExpiredBanner` | A `v-model:show` banner with `signIn`/`dismiss` emits. Bound to the `useSessionStore()` Pinia store (`src/stores/session.ts`), which `main.ts`'s `onAuthRefreshError` hook flips when the refresh token itself has expired |
|
|
178
178
|
|
|
179
179
|
There's no `AppHeader`/`AppFooter`/hero banner in this layout — `AppSideMenu` is the
|
|
@@ -186,7 +186,7 @@ marketing chrome around a staff tool).
|
|
|
186
186
|
| Component | Purpose |
|
|
187
187
|
|---|---|
|
|
188
188
|
| `AppHeader` | `goa-microsite-header` + `goa-app-header`. Takes a `heading` prop; put account/sign-in actions in its `utilities` slot (a bare child with no slot is silently dropped by `goa-app-header`) |
|
|
189
|
-
| `AppLayout` | The content gutter every view renders inside (see the key files table).
|
|
189
|
+
| `AppLayout` | The content gutter every view renders inside (see the key files table). `App.vue` itself owns the skip-to-main-content landmark for this layout — `AppLayout` deliberately doesn't duplicate it |
|
|
190
190
|
| `AppFooter` | `goa-app-footer` with the standard GoA nav/meta links (Services/Contact/Terms of Use, Privacy/Disclaimer/Accessibility) |
|
|
191
191
|
| `SessionExpiredBanner` | A `v-model:show` banner with `signIn`/`dismiss` emits. Bound to the `useSessionStore()` Pinia store (`src/stores/session.ts`), which `main.ts`'s `onAuthRefreshError` hook flips when the refresh token itself has expired |
|
|
192
192
|
<% } %>
|
|
@@ -201,6 +201,23 @@ composite generator.
|
|
|
201
201
|
|
|
202
202
|
## Adding a view
|
|
203
203
|
|
|
204
|
+
**Check whether a generator already covers this view before hand-authoring one.** Each
|
|
205
|
+
retrofits into this project directly — file(s), route(s), and the shared component it's
|
|
206
|
+
built on, all in one step:
|
|
207
|
+
|
|
208
|
+
| If the view is... | Use | Built on |
|
|
209
|
+
|---|---|---|
|
|
210
|
+
| A single record's detail page (loading/error, optional status badge, back button) | `nx g @abgov/nx-adsp:vue-detail-view` | `RecordDetailShell` |
|
|
211
|
+
| A staff-facing, paginated/sortable/filterable list | `nx g @abgov/nx-adsp:vue-workspace-view` | `WorkspaceTable` |
|
|
212
|
+
| A simple admin list + create/update edit screen (no pagination) | `nx g @abgov/nx-adsp:vue-admin-crud` | `WorkspaceTable` |
|
|
213
|
+
| A multi-step submission flow (route-per-step, review, confirmation) | `nx g @abgov/nx-adsp:vue-intake-view` | `Stepper`, `StepErrorSummary` |
|
|
214
|
+
|
|
215
|
+
Run `nx g @abgov/nx-adsp:<generator> --help` for its full option list (each takes a
|
|
216
|
+
`--fields`/`--columns`/`--steps` JSON-string spec, not a CLI array flag — see the
|
|
217
|
+
generator's own schema description for why). Only hand-author a view when none of these
|
|
218
|
+
match, or once a generator's output needs customization it doesn't cover — its own comments
|
|
219
|
+
mark what's meant to be edited.
|
|
220
|
+
|
|
204
221
|
1. Create `src/views/MyFeatureView.vue` as a `<script setup>` SFC. **Don't add your
|
|
205
222
|
own page margins/centering** — every view renders inside `AppLayout`'s gutter, so
|
|
206
223
|
any top-level tag (`<div>`, `<section>`, …) is already centered and padded.
|
|
@@ -64,6 +64,7 @@ function onAccountItemClick() {
|
|
|
64
64
|
</AppLayout>
|
|
65
65
|
</AppSideMenu>
|
|
66
66
|
<% } else { %>
|
|
67
|
+
<a class="skip-link" href="#main-content">Skip to main content</a>
|
|
67
68
|
<AppHeader heading="<%= projectName %>">
|
|
68
69
|
<template #utilities>
|
|
69
70
|
<goa-button-group alignment="end">
|
|
@@ -86,13 +87,34 @@ function onAccountItemClick() {
|
|
|
86
87
|
@dismiss="session.dismiss"
|
|
87
88
|
/>
|
|
88
89
|
<goa-hero-banner heading="<%= projectName %>" backgroundurl="/assets/banner.jpg" />
|
|
89
|
-
<
|
|
90
|
-
<
|
|
91
|
-
|
|
90
|
+
<main id="main-content">
|
|
91
|
+
<AppLayout :variant="contentWidth">
|
|
92
|
+
<RouterView />
|
|
93
|
+
</AppLayout>
|
|
94
|
+
</main>
|
|
92
95
|
<AppFooter />
|
|
93
96
|
<% } %>
|
|
94
97
|
</template>
|
|
95
98
|
|
|
96
99
|
<style>
|
|
97
100
|
body { margin: 0; }
|
|
101
|
+
<% if (layout !== 'internal') { %>
|
|
102
|
+
/* Visually hidden until focused (e.g. via Tab from the top of the page) — lets
|
|
103
|
+
keyboard/screen-reader users jump past the header straight to the content.
|
|
104
|
+
The internal/side-menu shell has its own scoped copy of this; this layout
|
|
105
|
+
has no equivalent shell component, so App.vue owns it directly here. */
|
|
106
|
+
.skip-link {
|
|
107
|
+
position: absolute;
|
|
108
|
+
left: -9999px;
|
|
109
|
+
top: 0;
|
|
110
|
+
z-index: 100;
|
|
111
|
+
padding: var(--goa-space-s, 0.5rem) var(--goa-space-m, 1rem);
|
|
112
|
+
background: var(--goa-color-interactive-default, #0070c4);
|
|
113
|
+
color: #fff;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.skip-link:focus {
|
|
117
|
+
left: var(--goa-space-m, 1rem);
|
|
118
|
+
}
|
|
119
|
+
<% } %>
|
|
98
120
|
</style>
|
|
@@ -124,14 +124,21 @@ describe('Vue App Generator', () => {
|
|
|
124
124
|
expect(layout).toContain('form-content');
|
|
125
125
|
expect(layout).toContain('wide-content');
|
|
126
126
|
expect(layout).toContain('--goa-space');
|
|
127
|
-
//
|
|
128
|
-
|
|
127
|
+
// AppLayout must NOT own the skip-to-main-content landmark itself: it nests
|
|
128
|
+
// inside AppSideMenu for --layout=internal, which already provides one, and
|
|
129
|
+
// a second <main id="main-content"> would duplicate the landmark/id.
|
|
130
|
+
expect(layout).not.toContain('id="main-content"');
|
|
131
|
+
expect(layout).not.toContain('skip-link');
|
|
129
132
|
|
|
130
133
|
// App.vue uses AppLayout and no longer relies on the `main > section` gutter
|
|
131
134
|
// (which silently failed when a view's top-level tag wasn't <section>).
|
|
135
|
+
// For --layout=header (the default), App.vue itself owns the skip-link
|
|
136
|
+
// landmark instead, since there's no shell component to hold it.
|
|
132
137
|
const app = host.read('apps/test/src/App.vue').toString();
|
|
133
138
|
expect(app).toContain('AppLayout');
|
|
134
139
|
expect(app).not.toContain('main > section');
|
|
140
|
+
expect(app).toContain('href="#main-content"');
|
|
141
|
+
expect(app).toContain('id="main-content"');
|
|
135
142
|
});
|
|
136
143
|
|
|
137
144
|
it('provisions the shared GoA wrapper library and points the app at it', async () => {
|
|
@@ -202,6 +209,11 @@ describe('Vue App Generator', () => {
|
|
|
202
209
|
expect(app).toContain('@item-click="onAccountItemClick"');
|
|
203
210
|
// The content gutter is shared regardless of shell choice.
|
|
204
211
|
expect(app).toContain('<AppLayout');
|
|
212
|
+
// App.vue itself must not add a second skip-to-main-content landmark —
|
|
213
|
+
// AppSideMenu (imported, not inlined) already owns the one and only
|
|
214
|
+
// #main-content for this layout.
|
|
215
|
+
expect(app).not.toContain('id="main-content"');
|
|
216
|
+
expect(app).not.toContain('skip-link');
|
|
205
217
|
|
|
206
218
|
const agents = host.read('apps/test/AGENTS.md').toString();
|
|
207
219
|
expect(agents).toContain('--layout=internal');
|
|
@@ -1,14 +1,31 @@
|
|
|
1
1
|
# AGENTS.md — vue-components (GoA Design System Vue wrappers + app shell)
|
|
2
2
|
|
|
3
|
-
A shared, workspace-local library
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
A shared, workspace-local library — every Vue app in this workspace imports from
|
|
4
|
+
it instead of carrying its own copy. Generated by `@abgov/nx-adsp:vue-components`,
|
|
5
|
+
invoked automatically by `vue-app` and by every `vue-*-view` generator.
|
|
6
6
|
|
|
7
7
|
| Folder | Contains | Lifespan |
|
|
8
8
|
|---|---|---|
|
|
9
9
|
| `src/lib/primitives/` | Thin `v-model`/idiomatic-event wrappers over individual `goa-*` elements (`GoabInput`, `GoabButton`, …) | **Interim** — see below |
|
|
10
10
|
| `src/lib/patterns/` | Composite, app-shell components (`AppLayout`, `AppHeader`, `AppFooter`, `AppSideMenu`, `SessionExpiredBanner`, `RecordDetailShell`, `WorkspaceTable`, `Stepper`, `StepErrorSummary`) | **Permanent** |
|
|
11
11
|
|
|
12
|
+
**Not every pattern component is present in every app.** `AppLayout`, `AppHeader`,
|
|
13
|
+
`AppFooter`, `AppSideMenu`, and `SessionExpiredBanner` are base app-shell — `vue-app`
|
|
14
|
+
provisions and wires all five into `App.vue` for every generated app, unconditionally.
|
|
15
|
+
The rest are on-demand: they only exist in a given app's `src/lib/patterns/` if that
|
|
16
|
+
app has run the matching view generator at least once.
|
|
17
|
+
|
|
18
|
+
| Component | Provisioned by |
|
|
19
|
+
|---|---|
|
|
20
|
+
| `RecordDetailShell` | `vue-detail-view` |
|
|
21
|
+
| `WorkspaceTable` | `vue-workspace-view`, `vue-admin-crud` |
|
|
22
|
+
| `Stepper`, `StepErrorSummary` | `vue-intake-view` |
|
|
23
|
+
|
|
24
|
+
Don't assume one of these on-demand components exists in a given app — check whether
|
|
25
|
+
its generator has actually been run (e.g. `nx list @abgov/nx-adsp` won't tell you;
|
|
26
|
+
look for the component file itself, or the view file that would import it) before
|
|
27
|
+
building on it.
|
|
28
|
+
|
|
12
29
|
> **⚠️ `primitives/` is interim — do not invest in it as permanent.** It exists
|
|
13
30
|
> only because GoA DS has not yet published an official Vue wrapper package. When
|
|
14
31
|
> `@abgov/vue-components` ships, delete `primitives/` and repoint imports at it —
|
|
@@ -8,37 +8,24 @@
|
|
|
8
8
|
// meta: { layout: 'form' } // single-column forms → 640px
|
|
9
9
|
// (default) // general content → 1000px
|
|
10
10
|
// meta: { layout: 'wide' } // data-heavy pages / tables → 1200px
|
|
11
|
+
//
|
|
12
|
+
// Deliberately not a landmark/skip-target itself: this component nests inside
|
|
13
|
+
// AppSideMenu for --layout=internal, which already provides one, and a second
|
|
14
|
+
// copy would duplicate the anchor screen readers/keyboard nav jump to. The
|
|
15
|
+
// top-level shell (App.vue for --layout=header, AppSideMenu for --layout=internal)
|
|
16
|
+
// owns that landmark instead.
|
|
11
17
|
withDefaults(defineProps<{ variant?: 'page' | 'wide' | 'form' }>(), {
|
|
12
18
|
variant: 'page',
|
|
13
19
|
});
|
|
14
20
|
</script>
|
|
15
21
|
|
|
16
22
|
<template>
|
|
17
|
-
<
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
<slot />
|
|
21
|
-
</div>
|
|
22
|
-
</main>
|
|
23
|
+
<div :class="`${variant}-content`">
|
|
24
|
+
<slot />
|
|
25
|
+
</div>
|
|
23
26
|
</template>
|
|
24
27
|
|
|
25
28
|
<style scoped>
|
|
26
|
-
/* Visually hidden until focused (e.g. via Tab from the top of the page) — lets
|
|
27
|
-
keyboard/screen-reader users jump past the header straight to the content. */
|
|
28
|
-
.skip-link {
|
|
29
|
-
position: absolute;
|
|
30
|
-
left: -9999px;
|
|
31
|
-
top: 0;
|
|
32
|
-
z-index: 100;
|
|
33
|
-
padding: var(--goa-space-s, 0.5rem) var(--goa-space-m, 1rem);
|
|
34
|
-
background: var(--goa-color-interactive-default, #0070c4);
|
|
35
|
-
color: #fff;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
.skip-link:focus {
|
|
39
|
-
left: var(--goa-space-m, 1rem);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
29
|
.page-content,
|
|
43
30
|
.wide-content,
|
|
44
31
|
.form-content {
|
|
@@ -1,9 +1,20 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
import { useSlots } from 'vue';
|
|
3
|
+
|
|
2
4
|
// The alternate top-level shell for staff-facing/internal apps (goa-work-side-menu),
|
|
3
5
|
// used instead of AppHeader/AppFooter -- not together with them. Presentational only:
|
|
4
6
|
// unlike the real GovAlta-Pronghorn source this is adapted from, active-route
|
|
5
7
|
// highlighting and navigation are the caller's job (pass `current` on each item,
|
|
6
8
|
// listen for `item-click`) so this component doesn't need vue-router as a dependency.
|
|
9
|
+
//
|
|
10
|
+
// The real reference source also has a slim in-content "topbar" row for
|
|
11
|
+
// module-registered header actions (e.g. a notification bell) -- there's no
|
|
12
|
+
// AppHeader/utilities slot in this layout to hold that kind of thing. Exposed
|
|
13
|
+
// here as a plain named slot (`#topbar`), not a module-registration system --
|
|
14
|
+
// that's app-specific plumbing, not something a shared, presentational
|
|
15
|
+
// component should own.
|
|
16
|
+
const slots = useSlots();
|
|
17
|
+
|
|
7
18
|
interface AppSideMenuItem {
|
|
8
19
|
label: string;
|
|
9
20
|
to?: string;
|
|
@@ -63,6 +74,9 @@ const emit = defineEmits<{ itemClick: [item: AppSideMenuItem] }>();
|
|
|
63
74
|
/>
|
|
64
75
|
</goa-work-side-menu>
|
|
65
76
|
<main id="main-content" class="app-side-menu-content">
|
|
77
|
+
<div v-if="slots.topbar" class="app-side-menu-topbar">
|
|
78
|
+
<slot name="topbar" />
|
|
79
|
+
</div>
|
|
66
80
|
<slot />
|
|
67
81
|
</main>
|
|
68
82
|
</div>
|
|
@@ -80,6 +94,15 @@ const emit = defineEmits<{ itemClick: [item: AppSideMenuItem] }>();
|
|
|
80
94
|
overflow-y: auto;
|
|
81
95
|
}
|
|
82
96
|
|
|
97
|
+
.app-side-menu-topbar {
|
|
98
|
+
display: flex;
|
|
99
|
+
justify-content: flex-end;
|
|
100
|
+
align-items: center;
|
|
101
|
+
gap: var(--goa-space-s, 0.75rem);
|
|
102
|
+
padding: var(--goa-space-xs, 0.5rem) var(--goa-space-m, 1rem);
|
|
103
|
+
border-bottom: 1px solid var(--goa-color-greyscale-100, #f1f1f1);
|
|
104
|
+
}
|
|
105
|
+
|
|
83
106
|
.skip-link {
|
|
84
107
|
position: absolute;
|
|
85
108
|
left: -9999px;
|
|
@@ -76,6 +76,19 @@ describe('Vue Components Generator', () => {
|
|
|
76
76
|
expect(agents).toContain('defineModel<boolean>');
|
|
77
77
|
}, 30000);
|
|
78
78
|
|
|
79
|
+
it('AppSideMenu exposes an optional #topbar slot for header-action-style content', async () => {
|
|
80
|
+
await generator(host);
|
|
81
|
+
|
|
82
|
+
const sideMenu = host
|
|
83
|
+
.read('libs/vue-components/src/lib/patterns/AppSideMenu.vue')
|
|
84
|
+
.toString();
|
|
85
|
+
// Named slot, only rendered when actually given content -- no empty bar
|
|
86
|
+
// shows by default, matching the "unused by default" doc claim.
|
|
87
|
+
expect(sideMenu).toContain('<slot name="topbar" />');
|
|
88
|
+
expect(sideMenu).toContain('v-if="slots.topbar"');
|
|
89
|
+
expect(sideMenu).toContain('useSlots');
|
|
90
|
+
}, 30000);
|
|
91
|
+
|
|
79
92
|
it('disables vue/no-deprecated-slot-attribute in flat config too, not just .eslintrc.json', async () => {
|
|
80
93
|
// useFlatConfig() (from @nx/eslint) treats a root flat-config file's
|
|
81
94
|
// presence as authoritative, regardless of the installed ESLint version —
|