@unopsitg/ux 21.1.0 → 21.1.1
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/AGENTS.md +72 -0
- package/package.json +5 -1
- package/schematics/collection.json +10 -0
- package/schematics/ng-add/index.d.ts +7 -0
- package/schematics/ng-add/index.js +222 -0
- package/schematics/ng-add/schema.json +21 -0
package/AGENTS.md
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# @unopsitg/ux — AI Agent Integration Guide
|
|
2
|
+
|
|
3
|
+
This file is for AI coding assistants (Cursor, Claude Code, Copilot, etc.) working in projects that depend on `@unopsitg/ux`.
|
|
4
|
+
|
|
5
|
+
## What this library provides
|
|
6
|
+
|
|
7
|
+
- **Layout shell** — full application chrome (sidebar, topbar, breadcrumb, configurator)
|
|
8
|
+
- **Brand theme** — PrimeNG / PrimeUIX preset (`BrandSoft`) with UNOPS brand colors
|
|
9
|
+
- **Tailwind CSS** — design tokens, custom utilities, and component animations
|
|
10
|
+
- **Shared types** — demo data interfaces
|
|
11
|
+
|
|
12
|
+
## Quick setup (or run `ng add @unopsitg/ux`)
|
|
13
|
+
|
|
14
|
+
1. Create `.postcssrc.json` (not `.mjs` — Angular 21 esbuild ignores `.mjs`):
|
|
15
|
+
```json
|
|
16
|
+
{ "plugins": { "@tailwindcss/postcss": {} } }
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
2. Create `src/tailwind.css`:
|
|
20
|
+
```css
|
|
21
|
+
@import "../node_modules/@unopsitg/ux/assets/tailwind.css";
|
|
22
|
+
@source "../node_modules/@unopsitg/ux/fesm2022";
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
3. In `angular.json` styles:
|
|
26
|
+
```json
|
|
27
|
+
["node_modules/@unopsitg/ux/assets/styles.scss", "src/tailwind.css", "node_modules/primeicons/primeicons.css", "src/styles.scss"]
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
4. In `angular.json` assets:
|
|
31
|
+
```json
|
|
32
|
+
{ "glob": "**/*", "input": "node_modules/@unopsitg/ux/assets/opp", "output": "assets/opp" }
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
5. In `app.config.ts` providers:
|
|
36
|
+
```typescript
|
|
37
|
+
import { providePrimeNG } from 'primeng/config';
|
|
38
|
+
import { BrandSoft, TOPBAR_PROFILE_MENU_CONFIG, LayoutService } from '@unopsitg/ux';
|
|
39
|
+
|
|
40
|
+
providePrimeNG({ theme: { preset: BrandSoft, options: { darkModeSelector: '.app-dark' } } })
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Critical rules
|
|
44
|
+
|
|
45
|
+
- **NEVER** put `@source` directives in `.scss` files — Sass passes them through as inert text.
|
|
46
|
+
- **NEVER** use `postcss.config.mjs` — Angular 21 esbuild silently ignores it.
|
|
47
|
+
- **NEVER** reference `node_modules/@unopsitg/ux/assets/tailwind.css` directly in `angular.json` — use the `src/tailwind.css` wrapper for correct `@source` path resolution.
|
|
48
|
+
- Shell-critical utilities (`.hidden`, `.animate-scalein`, `.animate-fadeout`) ship as real CSS. Do not redefine them.
|
|
49
|
+
|
|
50
|
+
## Injection tokens
|
|
51
|
+
|
|
52
|
+
| Token | Purpose | Shape |
|
|
53
|
+
|-------|---------|-------|
|
|
54
|
+
| `MENU_MODEL` | Sidebar menu tree | `MenuItem[]` |
|
|
55
|
+
| `SIDEBAR_LOGO` | Expanded/compact logo URLs | `{ expanded, compact, alt }` |
|
|
56
|
+
| `TOPBAR_MOBILE_LOGO` | Mobile header logos | `{ light, dark }` |
|
|
57
|
+
| `TOPBAR_PROFILE_MENU_CONFIG` | Profile dropdown items | `{ items: { id, label, icon, command?, separator? }[] }` |
|
|
58
|
+
|
|
59
|
+
## Theme initialization
|
|
60
|
+
|
|
61
|
+
`LayoutService` defaults to `darkTheme: true`. To avoid a flash of light mode, add an `APP_INITIALIZER`:
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
import { APP_INITIALIZER } from '@angular/core';
|
|
65
|
+
import { LayoutService } from '@unopsitg/ux';
|
|
66
|
+
|
|
67
|
+
{ provide: APP_INITIALIZER, useFactory: (ls: LayoutService) => () => ls.toggleDarkMode(), deps: [LayoutService], multi: true }
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Full documentation
|
|
71
|
+
|
|
72
|
+
See `README.md` in this package for complete configuration reference.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unopsitg/ux",
|
|
3
|
-
"version": "21.1.
|
|
3
|
+
"version": "21.1.1",
|
|
4
4
|
"description": "UNOPS Angular 21 layout shell, brand theme (PrimeNG / PrimeUIX), and shared types",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"angular",
|
|
@@ -9,6 +9,10 @@
|
|
|
9
9
|
"layout"
|
|
10
10
|
],
|
|
11
11
|
"license": "UNLICENSED",
|
|
12
|
+
"schematics": "./schematics/collection.json",
|
|
13
|
+
"ng-add": {
|
|
14
|
+
"save": "dependencies"
|
|
15
|
+
},
|
|
12
16
|
"repository": {
|
|
13
17
|
"type": "git",
|
|
14
18
|
"url": "https://github.com/opp_plus/unops-ng21_ux.git"
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../../../node_modules/@angular-devkit/schematics/collection-schema.json",
|
|
3
|
+
"schematics": {
|
|
4
|
+
"ng-add": {
|
|
5
|
+
"description": "Set up @unopsitg/ux in an Angular project",
|
|
6
|
+
"factory": "./ng-add/index#ngAdd",
|
|
7
|
+
"schema": "./ng-add/schema.json"
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ngAdd = ngAdd;
|
|
4
|
+
const schematics_1 = require("@angular-devkit/schematics");
|
|
5
|
+
const tasks_1 = require("@angular-devkit/schematics/tasks");
|
|
6
|
+
const PEER_DEPS = {
|
|
7
|
+
primeng: '^21.0.4',
|
|
8
|
+
'@primeuix/themes': '^2.0.0',
|
|
9
|
+
primeicons: '^7.0.0',
|
|
10
|
+
'@tailwindcss/postcss': '^4.0.0',
|
|
11
|
+
'tailwindcss': '^4.0.0',
|
|
12
|
+
};
|
|
13
|
+
const STYLES_ENTRIES = [
|
|
14
|
+
'node_modules/@unopsitg/ux/assets/styles.scss',
|
|
15
|
+
'src/tailwind.css',
|
|
16
|
+
'node_modules/primeicons/primeicons.css',
|
|
17
|
+
'src/styles.scss',
|
|
18
|
+
];
|
|
19
|
+
const ASSETS_ENTRY = {
|
|
20
|
+
glob: '**/*',
|
|
21
|
+
input: 'node_modules/@unopsitg/ux/assets/opp',
|
|
22
|
+
output: 'assets/opp',
|
|
23
|
+
};
|
|
24
|
+
function ngAdd(options) {
|
|
25
|
+
return (0, schematics_1.chain)([
|
|
26
|
+
addPeerDependencies(),
|
|
27
|
+
createPostcssConfig(),
|
|
28
|
+
createTailwindWrapper(),
|
|
29
|
+
patchAngularJson(options),
|
|
30
|
+
patchAppConfig(options),
|
|
31
|
+
createCursorRule(),
|
|
32
|
+
installDependencies(),
|
|
33
|
+
]);
|
|
34
|
+
}
|
|
35
|
+
function addPeerDependencies() {
|
|
36
|
+
return (tree) => {
|
|
37
|
+
const pkgPath = '/package.json';
|
|
38
|
+
const buffer = tree.read(pkgPath);
|
|
39
|
+
if (!buffer) {
|
|
40
|
+
throw new schematics_1.SchematicsException('Could not find package.json');
|
|
41
|
+
}
|
|
42
|
+
const pkg = JSON.parse(buffer.toString('utf-8'));
|
|
43
|
+
if (!pkg.dependencies) {
|
|
44
|
+
pkg.dependencies = {};
|
|
45
|
+
}
|
|
46
|
+
for (const [name, version] of Object.entries(PEER_DEPS)) {
|
|
47
|
+
if (!pkg.dependencies[name] && !pkg.devDependencies?.[name]) {
|
|
48
|
+
pkg.dependencies[name] = version;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
tree.overwrite(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
|
|
52
|
+
return tree;
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function createPostcssConfig() {
|
|
56
|
+
return (tree) => {
|
|
57
|
+
const path = '/.postcssrc.json';
|
|
58
|
+
if (tree.exists(path)) {
|
|
59
|
+
return tree;
|
|
60
|
+
}
|
|
61
|
+
tree.create(path, JSON.stringify({ plugins: { '@tailwindcss/postcss': {} } }, null, 2) + '\n');
|
|
62
|
+
return tree;
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function createTailwindWrapper() {
|
|
66
|
+
return (tree) => {
|
|
67
|
+
const path = '/src/tailwind.css';
|
|
68
|
+
if (tree.exists(path)) {
|
|
69
|
+
return tree;
|
|
70
|
+
}
|
|
71
|
+
tree.create(path, [
|
|
72
|
+
'@import "../node_modules/@unopsitg/ux/assets/tailwind.css";',
|
|
73
|
+
'@source "../node_modules/@unopsitg/ux/fesm2022";',
|
|
74
|
+
'',
|
|
75
|
+
].join('\n'));
|
|
76
|
+
return tree;
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function patchAngularJson(options) {
|
|
80
|
+
return (tree) => {
|
|
81
|
+
const angularJsonPath = '/angular.json';
|
|
82
|
+
const buffer = tree.read(angularJsonPath);
|
|
83
|
+
if (!buffer) {
|
|
84
|
+
throw new schematics_1.SchematicsException('Could not find angular.json');
|
|
85
|
+
}
|
|
86
|
+
const workspace = JSON.parse(buffer.toString('utf-8'));
|
|
87
|
+
const projectName = options.project || workspace.defaultProject || Object.keys(workspace.projects)[0];
|
|
88
|
+
const project = workspace.projects[projectName];
|
|
89
|
+
if (!project) {
|
|
90
|
+
throw new schematics_1.SchematicsException(`Project "${projectName}" not found in angular.json`);
|
|
91
|
+
}
|
|
92
|
+
const buildTarget = project.architect?.build || project.targets?.build;
|
|
93
|
+
if (!buildTarget) {
|
|
94
|
+
throw new schematics_1.SchematicsException(`No build target found for project "${projectName}"`);
|
|
95
|
+
}
|
|
96
|
+
const buildOptions = buildTarget.options || (buildTarget.options = {});
|
|
97
|
+
// Patch styles
|
|
98
|
+
const styles = buildOptions.styles || [];
|
|
99
|
+
const desiredStyles = STYLES_ENTRIES.filter((s) => !styles.includes(s));
|
|
100
|
+
if (desiredStyles.length > 0) {
|
|
101
|
+
const srcStylesIdx = styles.indexOf('src/styles.scss');
|
|
102
|
+
if (srcStylesIdx >= 0) {
|
|
103
|
+
// Insert library styles before src/styles.scss
|
|
104
|
+
const before = desiredStyles.filter((s) => s !== 'src/styles.scss');
|
|
105
|
+
styles.splice(srcStylesIdx, 0, ...before);
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
styles.push(...desiredStyles);
|
|
109
|
+
}
|
|
110
|
+
buildOptions.styles = styles;
|
|
111
|
+
}
|
|
112
|
+
// Patch assets
|
|
113
|
+
const assets = buildOptions.assets || [];
|
|
114
|
+
const hasOppAsset = assets.some((a) => typeof a === 'object' && a.input === ASSETS_ENTRY.input);
|
|
115
|
+
if (!hasOppAsset) {
|
|
116
|
+
assets.push(ASSETS_ENTRY);
|
|
117
|
+
buildOptions.assets = assets;
|
|
118
|
+
}
|
|
119
|
+
tree.overwrite(angularJsonPath, JSON.stringify(workspace, null, 2) + '\n');
|
|
120
|
+
return tree;
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
function patchAppConfig(options) {
|
|
124
|
+
return (tree) => {
|
|
125
|
+
const configPath = '/src/app/app.config.ts';
|
|
126
|
+
if (!tree.exists(configPath)) {
|
|
127
|
+
// Try alternate path
|
|
128
|
+
const altPath = '/src/app.config.ts';
|
|
129
|
+
if (!tree.exists(altPath)) {
|
|
130
|
+
return tree;
|
|
131
|
+
}
|
|
132
|
+
return patchConfigFile(tree, altPath, options);
|
|
133
|
+
}
|
|
134
|
+
return patchConfigFile(tree, configPath, options);
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
function patchConfigFile(tree, path, options) {
|
|
138
|
+
const content = tree.read(path).toString('utf-8');
|
|
139
|
+
if (content.includes('@unopsitg/ux')) {
|
|
140
|
+
return tree;
|
|
141
|
+
}
|
|
142
|
+
const darkMode = options.darkMode !== false;
|
|
143
|
+
const importBlock = [
|
|
144
|
+
"import { providePrimeNG } from 'primeng/config';",
|
|
145
|
+
"import { BrandSoft, TOPBAR_PROFILE_MENU_CONFIG, LayoutService } from '@unopsitg/ux';",
|
|
146
|
+
].join('\n');
|
|
147
|
+
const providerBlock = ` providePrimeNG({ theme: { preset: BrandSoft, options: { darkModeSelector: '.app-dark' } } }),`;
|
|
148
|
+
// Insert imports at the top (after existing imports)
|
|
149
|
+
const lastImportIdx = content.lastIndexOf('\nimport ');
|
|
150
|
+
let result;
|
|
151
|
+
if (lastImportIdx >= 0) {
|
|
152
|
+
const endOfImportLine = content.indexOf('\n', lastImportIdx + 1);
|
|
153
|
+
result =
|
|
154
|
+
content.slice(0, endOfImportLine + 1) +
|
|
155
|
+
importBlock +
|
|
156
|
+
'\n' +
|
|
157
|
+
content.slice(endOfImportLine + 1);
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
result = importBlock + '\n\n' + content;
|
|
161
|
+
}
|
|
162
|
+
// Insert provider into providers array
|
|
163
|
+
const providersMatch = result.match(/providers\s*:\s*\[/);
|
|
164
|
+
if (providersMatch && providersMatch.index != null) {
|
|
165
|
+
const insertPos = providersMatch.index + providersMatch[0].length;
|
|
166
|
+
result =
|
|
167
|
+
result.slice(0, insertPos) + '\n' + providerBlock + '\n' + result.slice(insertPos);
|
|
168
|
+
}
|
|
169
|
+
tree.overwrite(path, result);
|
|
170
|
+
return tree;
|
|
171
|
+
}
|
|
172
|
+
function createCursorRule() {
|
|
173
|
+
return (tree) => {
|
|
174
|
+
const rulePath = '/.cursor/rules/unopsitg-ux.mdc';
|
|
175
|
+
if (tree.exists(rulePath)) {
|
|
176
|
+
return tree;
|
|
177
|
+
}
|
|
178
|
+
const content = `---
|
|
179
|
+
description: Integration rules for @unopsitg/ux library
|
|
180
|
+
globs: ["**/*.{ts,html,scss,css,json}"]
|
|
181
|
+
alwaysApply: false
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
# @unopsitg/ux Integration
|
|
185
|
+
|
|
186
|
+
This project uses the @unopsitg/ux Angular library for its layout shell, brand theme, and shared components.
|
|
187
|
+
|
|
188
|
+
## Critical Integration Invariants
|
|
189
|
+
|
|
190
|
+
1. **PostCSS config must be JSON format** — use \`.postcssrc.json\`, never \`.mjs\`. Angular 21 esbuild silently ignores \`.mjs\` configs.
|
|
191
|
+
2. **Never put \`@source\` directives in \`.scss\` files** — Sass copies them as inert text. Use \`src/tailwind.css\` (a plain CSS file) for Tailwind directives.
|
|
192
|
+
3. **The \`src/tailwind.css\` wrapper** resolves \`@source\` paths from the project root where Angular runs PostCSS. Do not reference the library's \`assets/tailwind.css\` directly in \`angular.json\`.
|
|
193
|
+
4. **Shell utilities** (\`.hidden\`, \`.animate-scalein\`, \`.animate-fadeout\`) are shipped as real CSS in the library. Do not redefine them.
|
|
194
|
+
|
|
195
|
+
## Available Injection Tokens
|
|
196
|
+
|
|
197
|
+
| Token | Purpose | Shape |
|
|
198
|
+
|-------|---------|-------|
|
|
199
|
+
| \`MENU_MODEL\` | Sidebar menu tree | \`MenuItem[]\` |
|
|
200
|
+
| \`SIDEBAR_LOGO\` | Expanded/compact logo URLs | \`{ expanded, compact, alt }\` |
|
|
201
|
+
| \`TOPBAR_MOBILE_LOGO\` | Mobile header logos | \`{ light, dark }\` |
|
|
202
|
+
| \`TOPBAR_PROFILE_MENU_CONFIG\` | Profile dropdown items | \`{ items: { id, label, icon, command?, separator? }[] }\` |
|
|
203
|
+
|
|
204
|
+
## Theme
|
|
205
|
+
|
|
206
|
+
- \`LayoutService.layoutConfig()\` holds the active theme state.
|
|
207
|
+
- \`LayoutService.toggleDarkMode()\` synchronizes \`.app-dark\` on \`<html>\`.
|
|
208
|
+
- Use an \`APP_INITIALIZER\` to call \`toggleDarkMode()\` at startup to avoid a flash of wrong theme.
|
|
209
|
+
|
|
210
|
+
## Full Documentation
|
|
211
|
+
|
|
212
|
+
See \`node_modules/@unopsitg/ux/README.md\` for complete setup and configuration.
|
|
213
|
+
`;
|
|
214
|
+
tree.create(rulePath, content);
|
|
215
|
+
return tree;
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
function installDependencies() {
|
|
219
|
+
return (_tree, context) => {
|
|
220
|
+
context.addTask(new tasks_1.NodePackageInstallTask());
|
|
221
|
+
};
|
|
222
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema",
|
|
3
|
+
"$id": "SchematicsUnopsUxNgAdd",
|
|
4
|
+
"title": "UNOPS UX ng-add schematic",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"properties": {
|
|
7
|
+
"project": {
|
|
8
|
+
"type": "string",
|
|
9
|
+
"description": "The project to add @unopsitg/ux to.",
|
|
10
|
+
"$default": {
|
|
11
|
+
"$source": "projectName"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"darkMode": {
|
|
15
|
+
"type": "boolean",
|
|
16
|
+
"default": true,
|
|
17
|
+
"description": "Whether the app starts in dark mode."
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"required": []
|
|
21
|
+
}
|