@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 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.0",
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,7 @@
1
+ import { Rule } from '@angular-devkit/schematics';
2
+ interface Schema {
3
+ project?: string;
4
+ darkMode?: boolean;
5
+ }
6
+ export declare function ngAdd(options: Schema): Rule;
7
+ export {};
@@ -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
+ }