@unopsitg/ux 21.1.0 → 21.2.0

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,86 @@
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 (Tailwind v4 source file)
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 "tailwindcss";
22
+ @import "@unopsitg/ux/tailwind";
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. Install dev dependencies:
36
+ ```bash
37
+ npm install -D @tailwindcss/postcss tailwindcss postcss
38
+ ```
39
+
40
+ 6. In `app.config.ts` providers:
41
+ ```typescript
42
+ import { providePrimeNG } from 'primeng/config';
43
+ import { BrandSoft, TOPBAR_PROFILE_MENU_CONFIG, LayoutService } from '@unopsitg/ux';
44
+
45
+ providePrimeNG({ theme: { preset: BrandSoft, options: { darkModeSelector: '.app-dark' } } })
46
+ ```
47
+
48
+ ## Critical rules
49
+
50
+ - **NEVER** add `node_modules/@unopsitg/ux/assets/tailwind.css` directly to `angular.json` styles — Angular's esbuild does NOT run PostCSS on node_modules CSS, so all directives pass through as raw text and zero utilities are generated.
51
+ - **ALWAYS** use `src/tailwind.css` with `@import "@unopsitg/ux/tailwind"` — this lives in the source tree where PostCSS processes it.
52
+ - **NEVER** put `@source` directives in `.scss` files — Sass passes them through as inert text.
53
+ - **NEVER** use `postcss.config.mjs` — Angular 21 esbuild silently ignores it.
54
+ - Shell-critical utilities (`.hidden`, `.animate-scalein`, `.animate-fadeout`) ship as real CSS. Do not redefine them.
55
+
56
+ ## Package exports
57
+
58
+ | Import path | Resolves to |
59
+ |-------------|-------------|
60
+ | `@unopsitg/ux` | `fesm2022/unopsitg-ux.mjs` (Angular library) |
61
+ | `@unopsitg/ux/tailwind` | `assets/tailwind.css` (Tailwind v4 source) |
62
+ | `@unopsitg/ux/styles` | `assets/styles.scss` (layout SCSS) |
63
+
64
+ ## Injection tokens
65
+
66
+ | Token | Purpose | Shape |
67
+ |-------|---------|-------|
68
+ | `MENU_MODEL` | Sidebar menu tree | `MenuItem[]` |
69
+ | `SIDEBAR_LOGO` | Expanded/compact logo URLs | `{ expanded, compact, alt }` |
70
+ | `TOPBAR_MOBILE_LOGO` | Mobile header logos | `{ light, dark }` |
71
+ | `TOPBAR_PROFILE_MENU_CONFIG` | Profile dropdown items | `{ items: { id, label, icon, command?, separator? }[] }` |
72
+
73
+ ## Theme initialization
74
+
75
+ `LayoutService` defaults to `darkTheme: true`. To avoid a flash of light mode, add an `APP_INITIALIZER`:
76
+
77
+ ```typescript
78
+ import { APP_INITIALIZER } from '@angular/core';
79
+ import { LayoutService } from '@unopsitg/ux';
80
+
81
+ { provide: APP_INITIALIZER, useFactory: (ls: LayoutService) => () => ls.toggleDarkMode(), deps: [LayoutService], multi: true }
82
+ ```
83
+
84
+ ## Full documentation
85
+
86
+ See `README.md` in this package for complete configuration reference.
package/README.md CHANGED
@@ -44,11 +44,25 @@ export const appConfig: ApplicationConfig = {
44
44
 
45
45
  ## Styles and assets
46
46
 
47
- Reference library SCSS/Tailwind and copy bundled logos into your app output:
47
+ ### Automated setup
48
48
 
49
- ### PostCSS configuration
49
+ ```bash
50
+ ng add @unopsitg/ux
51
+ ```
52
+
53
+ This creates all required files and patches `angular.json` + `app.config.ts` automatically. The rest of this section documents the manual equivalent.
54
+
55
+ ### Understanding `assets/tailwind.css`
56
+
57
+ The library ships `assets/tailwind.css` as a **Tailwind v4 source file** — it contains `@plugin`, `@source`, `@theme`, and `@utility` directives that must be processed by `@tailwindcss/postcss` to generate utility classes.
50
58
 
51
- Angular 21's `application` builder (esbuild) only loads JSON-format PostCSS configs. Create `.postcssrc.json` in the project root:
59
+ > **Do NOT add `assets/tailwind.css` directly to angular.json styles.** Angular's esbuild/Vite builder does not run PostCSS on CSS files referenced from `node_modules` — it inlines them as raw text. All directives will appear literally in the browser output and zero utilities will be generated.
60
+
61
+ ### Manual setup
62
+
63
+ #### 1. PostCSS configuration
64
+
65
+ Angular 21's `application` builder only loads JSON-format PostCSS configs. Create `.postcssrc.json` in the project root:
52
66
 
53
67
  ```json
54
68
  {
@@ -58,9 +72,20 @@ Angular 21's `application` builder (esbuild) only loads JSON-format PostCSS conf
58
72
  }
59
73
  ```
60
74
 
61
- > **Warning:** `.mjs` configs (`postcss.config.mjs`) are silently ignored by esbuild. If Tailwind directives pass through unprocessed, check the config format first.
75
+ > **Warning:** `.mjs` configs (`postcss.config.mjs`) are silently ignored by esbuild.
76
+
77
+ #### 2. Tailwind entry point
78
+
79
+ Create `src/tailwind.css` — this lives in your source tree so Angular **will** run PostCSS on it:
80
+
81
+ ```css
82
+ @import "tailwindcss";
83
+ @import "@unopsitg/ux/tailwind";
84
+ ```
85
+
86
+ The `@unopsitg/ux/tailwind` export resolves to the library's `assets/tailwind.css` via the package `exports` field. The file includes brand tokens, custom utilities, and a `@source` directive that scans the library's compiled JS for class references.
62
87
 
63
- ### angular.json styles and assets
88
+ #### 3. angular.json styles and assets
64
89
 
65
90
  ```json
66
91
  "styles": [
@@ -75,23 +100,20 @@ Angular 21's `application` builder (esbuild) only loads JSON-format PostCSS conf
75
100
  ]
76
101
  ```
77
102
 
78
- ### Tailwind content scan
103
+ #### 4. Dev dependencies
79
104
 
80
- Library components use Tailwind utility classes. Tailwind 4 only generates utilities for classes it finds in scanned sources. The library's `assets/tailwind.css` contains a `@source "../fesm2022"` directive, but Angular resolves this relative to the **project root** — not the package directory. To fix path resolution, create a thin wrapper CSS file:
81
-
82
- ```css
83
- /* src/tailwind.css */
84
- @import "../node_modules/@unopsitg/ux/assets/tailwind.css";
85
- @source "../node_modules/@unopsitg/ux/fesm2022";
105
+ ```bash
106
+ npm install -D @tailwindcss/postcss tailwindcss postcss
86
107
  ```
87
108
 
88
- Reference `src/tailwind.css` in `angular.json` styles (as shown above) instead of the library file directly. The `@source` in your wrapper resolves from the project root where Angular actually runs PostCSS.
89
-
90
- > **Do not** put `@source` directives in `.scss` files — Sass copies them as inert text and PostCSS/Tailwind never processes them.
109
+ ### Troubleshooting
91
110
 
92
- **Verify:** after `ng serve`, check the compiled CSS for `.flex { display: flex }`. If missing, the `@source` path is wrong.
111
+ - **Tailwind directives appear as raw text in browser CSS** you added the library's CSS directly to `angular.json` instead of using `src/tailwind.css`.
112
+ - **"The path './assets/tailwind.css' is not exported"** — upgrade to `@unopsitg/ux@21.2.0+` which adds the `./tailwind` export.
113
+ - **Zero utilities generated** — verify `.postcssrc.json` exists (not `.mjs`) and `src/tailwind.css` is in the styles array.
114
+ - **Do not** put `@source` directives in `.scss` files — Sass copies them as inert text.
93
115
 
94
- When developing **inside this monorepo**, use paths under `projects/unops-ux/src/assets/` instead of `node_modules`.
116
+ **Verify:** after `ng serve`, inspect the compiled CSS for `.flex { display: flex }`. If missing, PostCSS is not running on your tailwind entry point.
95
117
 
96
118
  ## Tokens
97
119
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unopsitg/ux",
3
- "version": "21.1.0",
3
+ "version": "21.2.0",
4
4
  "description": "UNOPS Angular 21 layout shell, brand theme (PrimeNG / PrimeUIX), and shared types",
5
5
  "keywords": [
6
6
  "angular",
@@ -9,6 +9,21 @@
9
9
  "layout"
10
10
  ],
11
11
  "license": "UNLICENSED",
12
+ "schematics": "./schematics/collection.json",
13
+ "ng-add": {
14
+ "save": "dependencies"
15
+ },
16
+ "exports": {
17
+ "./tailwind": "./assets/tailwind.css",
18
+ "./styles": "./assets/styles.scss",
19
+ "./package.json": {
20
+ "default": "./package.json"
21
+ },
22
+ ".": {
23
+ "types": "./types/unopsitg-ux.d.ts",
24
+ "default": "./fesm2022/unopsitg-ux.mjs"
25
+ }
26
+ },
12
27
  "repository": {
13
28
  "type": "git",
14
29
  "url": "https://github.com/opp_plus/unops-ng21_ux.git"
@@ -29,15 +44,6 @@
29
44
  },
30
45
  "module": "fesm2022/unopsitg-ux.mjs",
31
46
  "typings": "types/unopsitg-ux.d.ts",
32
- "exports": {
33
- "./package.json": {
34
- "default": "./package.json"
35
- },
36
- ".": {
37
- "types": "./types/unopsitg-ux.d.ts",
38
- "default": "./fesm2022/unopsitg-ux.mjs"
39
- }
40
- },
41
47
  "sideEffects": false,
42
48
  "type": "module",
43
49
  "dependencies": {
@@ -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,241 @@
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 RUNTIME_DEPS = {
7
+ primeng: '^21.0.4',
8
+ '@primeuix/themes': '^2.0.0',
9
+ primeicons: '^7.0.0',
10
+ };
11
+ const DEV_DEPS = {
12
+ '@tailwindcss/postcss': '^4.0.0',
13
+ tailwindcss: '^4.0.0',
14
+ postcss: '^8.4.0',
15
+ };
16
+ const STYLES_ENTRIES = [
17
+ 'node_modules/@unopsitg/ux/assets/styles.scss',
18
+ 'src/tailwind.css',
19
+ 'node_modules/primeicons/primeicons.css',
20
+ 'src/styles.scss',
21
+ ];
22
+ const ASSETS_ENTRY = {
23
+ glob: '**/*',
24
+ input: 'node_modules/@unopsitg/ux/assets/opp',
25
+ output: 'assets/opp',
26
+ };
27
+ function ngAdd(options) {
28
+ return (0, schematics_1.chain)([
29
+ addPeerDependencies(),
30
+ createPostcssConfig(),
31
+ createTailwindWrapper(),
32
+ patchAngularJson(options),
33
+ patchAppConfig(options),
34
+ createCursorRule(),
35
+ installDependencies(),
36
+ ]);
37
+ }
38
+ function addPeerDependencies() {
39
+ return (tree) => {
40
+ const pkgPath = '/package.json';
41
+ const buffer = tree.read(pkgPath);
42
+ if (!buffer) {
43
+ throw new schematics_1.SchematicsException('Could not find package.json');
44
+ }
45
+ const pkg = JSON.parse(buffer.toString('utf-8'));
46
+ if (!pkg.dependencies) {
47
+ pkg.dependencies = {};
48
+ }
49
+ if (!pkg.devDependencies) {
50
+ pkg.devDependencies = {};
51
+ }
52
+ for (const [name, version] of Object.entries(RUNTIME_DEPS)) {
53
+ if (!pkg.dependencies[name] && !pkg.devDependencies[name]) {
54
+ pkg.dependencies[name] = version;
55
+ }
56
+ }
57
+ for (const [name, version] of Object.entries(DEV_DEPS)) {
58
+ if (!pkg.dependencies[name] && !pkg.devDependencies[name]) {
59
+ pkg.devDependencies[name] = version;
60
+ }
61
+ }
62
+ tree.overwrite(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
63
+ return tree;
64
+ };
65
+ }
66
+ function createPostcssConfig() {
67
+ return (tree) => {
68
+ const path = '/.postcssrc.json';
69
+ if (tree.exists(path)) {
70
+ return tree;
71
+ }
72
+ tree.create(path, JSON.stringify({ plugins: { '@tailwindcss/postcss': {} } }, null, 2) + '\n');
73
+ return tree;
74
+ };
75
+ }
76
+ function createTailwindWrapper() {
77
+ return (tree) => {
78
+ const path = '/src/tailwind.css';
79
+ if (tree.exists(path)) {
80
+ return tree;
81
+ }
82
+ tree.create(path, [
83
+ '@import "tailwindcss";',
84
+ '@import "@unopsitg/ux/tailwind";',
85
+ '',
86
+ ].join('\n'));
87
+ return tree;
88
+ };
89
+ }
90
+ function patchAngularJson(options) {
91
+ return (tree) => {
92
+ const angularJsonPath = '/angular.json';
93
+ const buffer = tree.read(angularJsonPath);
94
+ if (!buffer) {
95
+ throw new schematics_1.SchematicsException('Could not find angular.json');
96
+ }
97
+ const workspace = JSON.parse(buffer.toString('utf-8'));
98
+ const projectName = options.project || workspace.defaultProject || Object.keys(workspace.projects)[0];
99
+ const project = workspace.projects[projectName];
100
+ if (!project) {
101
+ throw new schematics_1.SchematicsException(`Project "${projectName}" not found in angular.json`);
102
+ }
103
+ const buildTarget = project.architect?.build || project.targets?.build;
104
+ if (!buildTarget) {
105
+ throw new schematics_1.SchematicsException(`No build target found for project "${projectName}"`);
106
+ }
107
+ const buildOptions = buildTarget.options || (buildTarget.options = {});
108
+ // Patch styles
109
+ const styles = buildOptions.styles || [];
110
+ const desiredStyles = STYLES_ENTRIES.filter((s) => !styles.includes(s));
111
+ if (desiredStyles.length > 0) {
112
+ const srcStylesIdx = styles.indexOf('src/styles.scss');
113
+ if (srcStylesIdx >= 0) {
114
+ // Insert library styles before src/styles.scss
115
+ const before = desiredStyles.filter((s) => s !== 'src/styles.scss');
116
+ styles.splice(srcStylesIdx, 0, ...before);
117
+ }
118
+ else {
119
+ styles.push(...desiredStyles);
120
+ }
121
+ buildOptions.styles = styles;
122
+ }
123
+ // Patch assets
124
+ const assets = buildOptions.assets || [];
125
+ const hasOppAsset = assets.some((a) => typeof a === 'object' && a.input === ASSETS_ENTRY.input);
126
+ if (!hasOppAsset) {
127
+ assets.push(ASSETS_ENTRY);
128
+ buildOptions.assets = assets;
129
+ }
130
+ tree.overwrite(angularJsonPath, JSON.stringify(workspace, null, 2) + '\n');
131
+ return tree;
132
+ };
133
+ }
134
+ function patchAppConfig(options) {
135
+ return (tree) => {
136
+ const configPath = '/src/app/app.config.ts';
137
+ if (!tree.exists(configPath)) {
138
+ // Try alternate path
139
+ const altPath = '/src/app.config.ts';
140
+ if (!tree.exists(altPath)) {
141
+ return tree;
142
+ }
143
+ return patchConfigFile(tree, altPath, options);
144
+ }
145
+ return patchConfigFile(tree, configPath, options);
146
+ };
147
+ }
148
+ function patchConfigFile(tree, path, options) {
149
+ const content = tree.read(path).toString('utf-8');
150
+ if (content.includes('@unopsitg/ux')) {
151
+ return tree;
152
+ }
153
+ const darkMode = options.darkMode !== false;
154
+ const importBlock = [
155
+ "import { providePrimeNG } from 'primeng/config';",
156
+ "import { BrandSoft, TOPBAR_PROFILE_MENU_CONFIG, LayoutService } from '@unopsitg/ux';",
157
+ ].join('\n');
158
+ const providerBlock = ` providePrimeNG({ theme: { preset: BrandSoft, options: { darkModeSelector: '.app-dark' } } }),`;
159
+ // Insert imports at the top (after existing imports)
160
+ const lastImportIdx = content.lastIndexOf('\nimport ');
161
+ let result;
162
+ if (lastImportIdx >= 0) {
163
+ const endOfImportLine = content.indexOf('\n', lastImportIdx + 1);
164
+ result =
165
+ content.slice(0, endOfImportLine + 1) +
166
+ importBlock +
167
+ '\n' +
168
+ content.slice(endOfImportLine + 1);
169
+ }
170
+ else {
171
+ result = importBlock + '\n\n' + content;
172
+ }
173
+ // Insert provider into providers array
174
+ const providersMatch = result.match(/providers\s*:\s*\[/);
175
+ if (providersMatch && providersMatch.index != null) {
176
+ const insertPos = providersMatch.index + providersMatch[0].length;
177
+ result =
178
+ result.slice(0, insertPos) + '\n' + providerBlock + '\n' + result.slice(insertPos);
179
+ }
180
+ tree.overwrite(path, result);
181
+ return tree;
182
+ }
183
+ function createCursorRule() {
184
+ return (tree) => {
185
+ const rulePath = '/.cursor/rules/unopsitg-ux.mdc';
186
+ if (tree.exists(rulePath)) {
187
+ return tree;
188
+ }
189
+ const content = `---
190
+ description: Integration rules for @unopsitg/ux library
191
+ globs: ["**/*.{ts,html,scss,css,json}"]
192
+ alwaysApply: false
193
+ ---
194
+
195
+ # @unopsitg/ux Integration
196
+
197
+ This project uses the @unopsitg/ux Angular library for its layout shell, brand theme, and shared components.
198
+
199
+ ## Critical Integration Invariants
200
+
201
+ 1. **PostCSS config must be JSON format** — use \`.postcssrc.json\`, never \`.mjs\`. Angular 21 esbuild silently ignores \`.mjs\` configs.
202
+ 2. **\`assets/tailwind.css\` is a Tailwind v4 source file** (not pre-compiled CSS). It contains \`@plugin\`, \`@source\`, \`@theme\`, and \`@utility\` directives that must be processed by \`@tailwindcss/postcss\`. Never add it directly to \`angular.json\` styles — it will be inlined as raw text with no utility generation.
203
+ 3. **Use \`src/tailwind.css\`** as the entry point. It imports \`tailwindcss\` and \`@unopsitg/ux/tailwind\` via the package exports. This file IS processed by PostCSS because it lives in \`src/\`.
204
+ 4. **Never put \`@source\` directives in \`.scss\` files** — Sass copies them as inert text.
205
+ 5. **Shell utilities** (\`.hidden\`, \`.animate-scalein\`, \`.animate-fadeout\`) are shipped as real CSS in the library. Do not redefine them.
206
+
207
+ ## Correct src/tailwind.css
208
+
209
+ \`\`\`css
210
+ @import "tailwindcss";
211
+ @import "@unopsitg/ux/tailwind";
212
+ \`\`\`
213
+
214
+ ## Available Injection Tokens
215
+
216
+ | Token | Purpose | Shape |
217
+ |-------|---------|-------|
218
+ | \`MENU_MODEL\` | Sidebar menu tree | \`MenuItem[]\` |
219
+ | \`SIDEBAR_LOGO\` | Expanded/compact logo URLs | \`{ expanded, compact, alt }\` |
220
+ | \`TOPBAR_MOBILE_LOGO\` | Mobile header logos | \`{ light, dark }\` |
221
+ | \`TOPBAR_PROFILE_MENU_CONFIG\` | Profile dropdown items | \`{ items: { id, label, icon, command?, separator? }[] }\` |
222
+
223
+ ## Theme
224
+
225
+ - \`LayoutService.layoutConfig()\` holds the active theme state.
226
+ - \`LayoutService.toggleDarkMode()\` synchronizes \`.app-dark\` on \`<html>\`.
227
+ - Use an \`APP_INITIALIZER\` to call \`toggleDarkMode()\` at startup to avoid a flash of wrong theme.
228
+
229
+ ## Full Documentation
230
+
231
+ See \`node_modules/@unopsitg/ux/README.md\` for complete setup and configuration.
232
+ `;
233
+ tree.create(rulePath, content);
234
+ return tree;
235
+ };
236
+ }
237
+ function installDependencies() {
238
+ return (_tree, context) => {
239
+ context.addTask(new tasks_1.NodePackageInstallTask());
240
+ };
241
+ }
@@ -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
+ }