@chill-sharp/create-app 1.1.12
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/README.md +12 -0
- package/bin/create-chill-sharp-app.mjs +60 -0
- package/package.json +36 -0
- package/template/.agents/skills/chillsharp-current-user-preferences/SKILL.md +70 -0
- package/template/.agents/skills/chillsharp-ui-template/SKILL.md +18 -0
- package/template/.editorconfig +11 -0
- package/template/.github/workflows/ci.yml +33 -0
- package/template/.github/workflows/deploy.yml +36 -0
- package/template/angular.json +131 -0
- package/template/doc/CurrentUserPreferences.md +106 -0
- package/template/doc/it/CurrentUserPreferences.md +106 -0
- package/template/gitignore +6 -0
- package/template/package.json +43 -0
- package/template/public/env.js +15 -0
- package/template/public/fonts/material-symbols.woff +0 -0
- package/template/public/fonts/material-symbols.woff2 +0 -0
- package/template/public/runtime-config.js +10 -0
- package/template/public/sw.js +8 -0
- package/template/src/app/app.component.spec.ts +15 -0
- package/template/src/app/app.component.ts +10 -0
- package/template/src/app/app.config.ts +22 -0
- package/template/src/app/app.routes.ts +14 -0
- package/template/src/app/core/overrides/README.md +12 -0
- package/template/src/app/core/overrides/register-client-overrides.ts +5 -0
- package/template/src/app/core/plugins/README.md +17 -0
- package/template/src/app/core/plugins/hello-plugin/README.md +25 -0
- package/template/src/app/core/plugins/hello-plugin/hello-plugin.component.ts +28 -0
- package/template/src/app/core/plugins/register-client-plugins.ts +16 -0
- package/template/src/app/core/providers/client-template.providers.ts +8 -0
- package/template/src/app/pages/client-home/client-home.component.ts +41 -0
- package/template/src/assets/branding/logo.svg +7 -0
- package/template/src/config/app-config.ts +17 -0
- package/template/src/config/runtime-config.ts +13 -0
- package/template/src/environments/environment.prod.ts +4 -0
- package/template/src/environments/environment.ts +4 -0
- package/template/src/index.html +22 -0
- package/template/src/main.ts +6 -0
- package/template/src/runtime-config.d.ts +10 -0
- package/template/src/styles.scss +116 -0
- package/template/tsconfig.app.json +13 -0
- package/template/tsconfig.json +33 -0
- package/template/tsconfig.spec.json +13 -0
package/README.md
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Create ChillSharp App
|
|
2
|
+
|
|
3
|
+
Creates a ready-to-run Angular application shell backed by the published ChillSharp npm packages.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm create @chill-sharp/app my-app
|
|
7
|
+
cd my-app
|
|
8
|
+
npm install
|
|
9
|
+
npm start
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
The generator copies the client-owned application shell and configures it to use `@chill-sharp/ts-client`, `@chill-sharp/ng-client`, and `@chill-sharp/ui-core` from npm.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { copyFileSync, cpSync, existsSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
|
|
7
|
+
const packageDirectory = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
8
|
+
const templateDirectory = path.join(packageDirectory, 'template');
|
|
9
|
+
const packageManifest = JSON.parse(readFileSync(path.join(packageDirectory, 'package.json'), 'utf8'));
|
|
10
|
+
const [projectName] = process.argv.slice(2).filter((argument) => !argument.startsWith('-'));
|
|
11
|
+
|
|
12
|
+
if (!projectName || process.argv.includes('--help') || process.argv.includes('-h')) {
|
|
13
|
+
printUsage();
|
|
14
|
+
process.exit(projectName ? 0 : 1);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (!/^[a-z0-9][a-z0-9-]*$/.test(projectName)) {
|
|
18
|
+
console.error('Project names must use lowercase letters, digits, and hyphens only.');
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const destination = path.resolve(process.cwd(), projectName);
|
|
23
|
+
if (existsSync(destination)) {
|
|
24
|
+
console.error(`Cannot create '${projectName}': '${destination}' already exists.`);
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
cpSync(templateDirectory, destination, { recursive: true });
|
|
29
|
+
|
|
30
|
+
const generatedManifestPath = path.join(destination, 'package.json');
|
|
31
|
+
const generatedManifest = JSON.parse(readFileSync(generatedManifestPath, 'utf8'));
|
|
32
|
+
generatedManifest.name = projectName;
|
|
33
|
+
generatedManifest.private = true;
|
|
34
|
+
generatedManifest.description = 'Angular application powered by ChillSharp UI.';
|
|
35
|
+
generatedManifest.dependencies = {
|
|
36
|
+
...generatedManifest.dependencies,
|
|
37
|
+
'@chill-sharp/ng-client': `^${packageManifest.version}`,
|
|
38
|
+
'@chill-sharp/ts-client': `^${packageManifest.version}`,
|
|
39
|
+
'@chill-sharp/ui-core': `^${packageManifest.version}`
|
|
40
|
+
};
|
|
41
|
+
writeFileSync(generatedManifestPath, `${JSON.stringify(generatedManifest, null, 2)}\n`);
|
|
42
|
+
|
|
43
|
+
rmSync(path.join(destination, 'package-lock.json'), { force: true });
|
|
44
|
+
rmSync(path.join(destination, 'upgrade.ps1'), { force: true });
|
|
45
|
+
rmSync(path.join(destination, 'upgrade.sh'), { force: true });
|
|
46
|
+
rmSync(path.join(destination, 'publish-to-shared-folder.ps1'), { force: true });
|
|
47
|
+
copyFileSync(path.join(destination, 'gitignore'), path.join(destination, '.gitignore'));
|
|
48
|
+
rmSync(path.join(destination, 'gitignore'), { force: true });
|
|
49
|
+
writeFileSync(path.join(destination, 'README.md'), `# ${projectName}\n\nAngular application shell powered by ChillSharp UI.\n\n## Get started\n\n\`\`\`bash\nnpm install\nnpm start\n\`\`\`\n\nThe application uses published ChillSharp packages from npm. Update the shared UI explicitly with:\n\n\`\`\`bash\nnpm install @chill-sharp/ui-core@latest\n\`\`\`\n\nConfigure the API and client URLs in \`public/env.js\` and \`public/runtime-config.js\`.\n`);
|
|
50
|
+
writeFileSync(path.join(destination, 'HOW_TO_BUILD.md'), `# Build ${projectName}\n\n\`\`\`bash\nnpm install\nnpm run build\n\`\`\`\n\nThe ChillSharp dependencies are restored from npm; no local package archives are required.\n`);
|
|
51
|
+
|
|
52
|
+
console.log(`Created ${projectName}.`);
|
|
53
|
+
console.log('Next steps:');
|
|
54
|
+
console.log(` cd ${projectName}`);
|
|
55
|
+
console.log(' npm install');
|
|
56
|
+
console.log(' npm start');
|
|
57
|
+
|
|
58
|
+
function printUsage() {
|
|
59
|
+
console.log('Usage: npm create @chill-sharp/app <project-name>');
|
|
60
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@chill-sharp/create-app",
|
|
3
|
+
"repository": {
|
|
4
|
+
"type": "git",
|
|
5
|
+
"url": "git+https://github.com/e-500/chill-sharp.git",
|
|
6
|
+
"directory": "extra/chill-sharp-create-app"
|
|
7
|
+
},
|
|
8
|
+
"version": "1.1.12",
|
|
9
|
+
"description": "Create an Angular application powered by ChillSharp UI.",
|
|
10
|
+
"license": "AGPL-3.0-or-later",
|
|
11
|
+
"type": "module",
|
|
12
|
+
"bin": {
|
|
13
|
+
"create-chill-sharp-app": "./bin/create-chill-sharp-app.mjs"
|
|
14
|
+
},
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "node --check bin/create-chill-sharp-app.mjs"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"bin",
|
|
20
|
+
"template",
|
|
21
|
+
"README.md"
|
|
22
|
+
],
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=20"
|
|
25
|
+
},
|
|
26
|
+
"keywords": [
|
|
27
|
+
"chillsharp",
|
|
28
|
+
"angular",
|
|
29
|
+
"starter",
|
|
30
|
+
"generator"
|
|
31
|
+
],
|
|
32
|
+
"author": "Andrea Piovesan",
|
|
33
|
+
"publishConfig": {
|
|
34
|
+
"access": "public"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: chillsharp-current-user-preferences
|
|
3
|
+
description: Configure, expose, and consume ChillSharp.Auth's cached current-user culture, time zone, date-format, number-format, and theme preferences in server logic and UI clients.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# ChillSharp Current User Preferences
|
|
7
|
+
|
|
8
|
+
Use this skill when a ChillSharp feature needs the authenticated user's display culture, time zone, date format, or number format. It covers server-side lifecycle hooks and UI/client consumption of the authoritative `AuthUser` preferences. Do not use it for browser-only preferences unrelated to `AuthUser`.
|
|
9
|
+
|
|
10
|
+
## Model
|
|
11
|
+
|
|
12
|
+
`ChillUserPreferences` is an immutable snapshot with `DisplayCultureName`, `DisplayTimeZone`, `DisplayDateFormat`, `DisplayNumberFormat`, and `PreferredTheme`. The backend stores `PreferredTheme` as an opaque string. In UI Core, unauthenticated users follow browser `prefers-color-scheme`; clients register extra themes with `provideChillSharpUiCore({ additionalThemes: ['theme-name'] })`. Entity hooks read the snapshot synchronously through:
|
|
13
|
+
|
|
14
|
+
```csharp
|
|
15
|
+
var preferences = context.GetCurrentUserPreferences();
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Never load `AuthUser` from an `OnCreate`, `OnUpdate`, or other lifecycle hook merely to obtain these values.
|
|
19
|
+
|
|
20
|
+
## Auth integration
|
|
21
|
+
|
|
22
|
+
`AddChillAuthApi` registers `IChillAuthUserPreferencesAccessor` and the singleton `IChillAuthUserPreferencesCache`. Login and token refresh warm the snapshot; `ChillAuthService` refreshes it after an `AuthUser` save and removes it for deleted or renamed external identities.
|
|
23
|
+
|
|
24
|
+
For a custom host DbContext, inject the scoped accessor and implement the two `IChillContext` methods below. Keep the accessor optional only when the context must also be constructible outside DI, such as for migrations or tests.
|
|
25
|
+
|
|
26
|
+
```csharp
|
|
27
|
+
private readonly IChillAuthUserPreferencesAccessor? _userPreferencesAccessor;
|
|
28
|
+
|
|
29
|
+
public ChillUserPreferences GetCurrentUserPreferences() =>
|
|
30
|
+
_userPreferencesAccessor?.Current ?? ChillUserPreferences.Empty;
|
|
31
|
+
|
|
32
|
+
public string GetDefaultUserCultureName()
|
|
33
|
+
{
|
|
34
|
+
var cultureName = GetCurrentUserPreferences().DisplayCultureName;
|
|
35
|
+
return string.IsNullOrWhiteSpace(cultureName) ? GetPrimaryCultureName() : cultureName;
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
The accessor is cache-only: when there is no authenticated principal or no warmed snapshot, it returns `ChillUserPreferences.Empty`. Do not add a fallback database query to lifecycle hooks.
|
|
40
|
+
|
|
41
|
+
## API and client use
|
|
42
|
+
|
|
43
|
+
The authenticated endpoint `GET /api/chill-auth/current-user-preferences` returns the same `ChillUserPreferences` snapshot as JSON:
|
|
44
|
+
|
|
45
|
+
```json
|
|
46
|
+
{
|
|
47
|
+
"displayCultureName": "it-IT",
|
|
48
|
+
"displayTimeZone": "Europe/Rome",
|
|
49
|
+
"displayDateFormat": "dd/MM/yyyy",
|
|
50
|
+
"displayNumberFormat": "N2"
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Use this endpoint after authentication and when restoring a session; do not infer the active display culture or time zone from the browser, operating system, token claims, or auth-management user endpoints. The C#, Python, TypeScript, Angular, Vue, and React clients expose it as `GetCurrentUserPreferences`, `get_current_user_preferences`, or `getCurrentUserPreferences`; the Vue and React packages also provide `useCurrentUserPreferences`.
|
|
55
|
+
|
|
56
|
+
In Angular UI Core, inject `ChillService` and read its `userPreferences` signal (or its `displayCultureName`, `displayTimeZone`, `displayDateFormat`, and `displayNumberFormat` projections). Use these values to select UI language, format and parse dates and numbers, and convert UTC timestamps. Treat all fields as optional and choose an explicit fallback only when needed.
|
|
57
|
+
|
|
58
|
+
## Entity use
|
|
59
|
+
|
|
60
|
+
Use `DisplayTimeZone` as an IANA zone identifier only after validating it with the application's time-zone policy. Treat all fields as optional and provide an explicit fallback where one is required.
|
|
61
|
+
|
|
62
|
+
```csharp
|
|
63
|
+
public override void OnUpdate(IChillContext context)
|
|
64
|
+
{
|
|
65
|
+
var timeZone = context.GetCurrentUserPreferences().DisplayTimeZone;
|
|
66
|
+
// Apply application-specific behavior only when timeZone is available.
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
For full setup and cache behavior, read [CurrentUserPreferences.md](../../../doc/CurrentUserPreferences.md).
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: chillsharp-ui-template
|
|
3
|
+
description: Build and customize Angular ChillSharp client projects from the UI template, including plugins, overrides, i18n, permissions, and runtime configuration.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# ChillSharp UI Client Template
|
|
7
|
+
|
|
8
|
+
Use this skill for work in `chill-sharp-ui-template` or a client repository created from it.
|
|
9
|
+
|
|
10
|
+
The template owns runtime configuration, branding, routes, client plugins, and overrides. Shared controls, data access, schema rendering, auth, i18n primitives, and permission evaluation belong in `@chill-sharp/ui-core` and the ChillSharp client packages. Do not copy or patch shared package internals.
|
|
11
|
+
|
|
12
|
+
Keep `provideClientTemplateProviders()` in the app provider chain. Register feature routes in `src/app/core/plugins/register-client-plugins.ts`; register deliberate provider replacements in `src/app/core/overrides/register-client-overrides.ts`. Use public package APIs only.
|
|
13
|
+
|
|
14
|
+
Treat backend schema as the source of entity/property metadata rather than duplicating it in Angular. Use runtime API configuration from `public/env.js`. Request localized text with an explicit culture and preserve backend fallback behavior. UI permission checks can hide or disable operations, but the protected backend is the enforcement boundary.
|
|
15
|
+
|
|
16
|
+
Permission-sensitive features must account for `Module -> Entity -> Property`: entity `Query/Create/Update/Delete` is separate from property `See/Modify`. Never infer write access from a visible field.
|
|
17
|
+
|
|
18
|
+
Add plugins under the client-owned plugin folder, register providers through the override/provider points, keep tenant/feature flags/branding in runtime config, add focused tests, and run `npm run build` after registration changes. This `.agents/skills` directory travels with the template into client repositories.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
pull_request:
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
build-and-test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
|
|
13
|
+
steps:
|
|
14
|
+
- name: Checkout
|
|
15
|
+
uses: actions/checkout@v4
|
|
16
|
+
|
|
17
|
+
- name: Setup Node
|
|
18
|
+
uses: actions/setup-node@v4
|
|
19
|
+
with:
|
|
20
|
+
node-version: 22
|
|
21
|
+
cache: npm
|
|
22
|
+
|
|
23
|
+
- name: Install
|
|
24
|
+
run: npm ci
|
|
25
|
+
|
|
26
|
+
- name: Lint
|
|
27
|
+
run: npm run lint
|
|
28
|
+
|
|
29
|
+
- name: Build
|
|
30
|
+
run: npm run build
|
|
31
|
+
|
|
32
|
+
- name: Test
|
|
33
|
+
run: npm run test -- --browsers=ChromeHeadless
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
name: Deploy
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_dispatch:
|
|
5
|
+
push:
|
|
6
|
+
tags:
|
|
7
|
+
- "v*"
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
deploy:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
|
|
13
|
+
steps:
|
|
14
|
+
- name: Checkout
|
|
15
|
+
uses: actions/checkout@v4
|
|
16
|
+
|
|
17
|
+
- name: Setup Node
|
|
18
|
+
uses: actions/setup-node@v4
|
|
19
|
+
with:
|
|
20
|
+
node-version: 22
|
|
21
|
+
cache: npm
|
|
22
|
+
|
|
23
|
+
- name: Install
|
|
24
|
+
run: npm ci
|
|
25
|
+
|
|
26
|
+
- name: Build
|
|
27
|
+
run: npm run build
|
|
28
|
+
|
|
29
|
+
- name: Publish artifact
|
|
30
|
+
uses: actions/upload-artifact@v4
|
|
31
|
+
with:
|
|
32
|
+
name: client-ui-build
|
|
33
|
+
path: dist/chill-sharp-ui-template
|
|
34
|
+
|
|
35
|
+
- name: Deploy placeholder
|
|
36
|
+
run: echo "Replace this step with the target hosting deployment command."
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
|
3
|
+
"version": 1,
|
|
4
|
+
"newProjectRoot": "projects",
|
|
5
|
+
"projects": {
|
|
6
|
+
"chill-sharp-ui-template": {
|
|
7
|
+
"projectType": "application",
|
|
8
|
+
"schematics": {
|
|
9
|
+
"@schematics/angular:component": {
|
|
10
|
+
"style": "scss"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"root": "",
|
|
14
|
+
"sourceRoot": "src",
|
|
15
|
+
"prefix": "app",
|
|
16
|
+
"architect": {
|
|
17
|
+
"build": {
|
|
18
|
+
"builder": "@angular-devkit/build-angular:application",
|
|
19
|
+
"options": {
|
|
20
|
+
"outputPath": "dist/chill-sharp-ui-template",
|
|
21
|
+
"index": "src/index.html",
|
|
22
|
+
"browser": "src/main.ts",
|
|
23
|
+
"polyfills": [
|
|
24
|
+
"zone.js"
|
|
25
|
+
],
|
|
26
|
+
"tsConfig": "tsconfig.app.json",
|
|
27
|
+
"inlineStyleLanguage": "scss",
|
|
28
|
+
"assets": [
|
|
29
|
+
{
|
|
30
|
+
"glob": "*.js",
|
|
31
|
+
"input": "node_modules/@chill-sharp/ui-core/service-worker",
|
|
32
|
+
"output": "/"
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"glob": "**/*",
|
|
36
|
+
"input": "public"
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"glob": "**/*",
|
|
40
|
+
"input": "src/assets",
|
|
41
|
+
"output": "assets"
|
|
42
|
+
}
|
|
43
|
+
],
|
|
44
|
+
"styles": [
|
|
45
|
+
"src/styles.scss"
|
|
46
|
+
],
|
|
47
|
+
"scripts": []
|
|
48
|
+
},
|
|
49
|
+
"configurations": {
|
|
50
|
+
"production": {
|
|
51
|
+
"fileReplacements": [
|
|
52
|
+
{
|
|
53
|
+
"replace": "src/environments/environment.ts",
|
|
54
|
+
"with": "src/environments/environment.prod.ts"
|
|
55
|
+
}
|
|
56
|
+
],
|
|
57
|
+
"budgets": [
|
|
58
|
+
{
|
|
59
|
+
"type": "initial",
|
|
60
|
+
"maximumWarning": "500kB",
|
|
61
|
+
"maximumError": "1.5MB"
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"type": "anyComponentStyle",
|
|
65
|
+
"maximumWarning": "14kB",
|
|
66
|
+
"maximumError": "20kB"
|
|
67
|
+
}
|
|
68
|
+
],
|
|
69
|
+
"outputHashing": "all"
|
|
70
|
+
},
|
|
71
|
+
"development": {
|
|
72
|
+
"optimization": false,
|
|
73
|
+
"extractLicenses": false,
|
|
74
|
+
"sourceMap": true
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
"defaultConfiguration": "production"
|
|
78
|
+
},
|
|
79
|
+
"serve": {
|
|
80
|
+
"builder": "@angular-devkit/build-angular:dev-server",
|
|
81
|
+
"options": {
|
|
82
|
+
"port": 6202
|
|
83
|
+
},
|
|
84
|
+
"configurations": {
|
|
85
|
+
"production": {
|
|
86
|
+
"buildTarget": "chill-sharp-ui-template:build:production"
|
|
87
|
+
},
|
|
88
|
+
"development": {
|
|
89
|
+
"buildTarget": "chill-sharp-ui-template:build:development"
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
"defaultConfiguration": "development"
|
|
93
|
+
},
|
|
94
|
+
"extract-i18n": {
|
|
95
|
+
"builder": "@angular-devkit/build-angular:extract-i18n"
|
|
96
|
+
},
|
|
97
|
+
"test": {
|
|
98
|
+
"builder": "@angular-devkit/build-angular:karma",
|
|
99
|
+
"options": {
|
|
100
|
+
"polyfills": [
|
|
101
|
+
"zone.js",
|
|
102
|
+
"zone.js/testing"
|
|
103
|
+
],
|
|
104
|
+
"tsConfig": "tsconfig.spec.json",
|
|
105
|
+
"inlineStyleLanguage": "scss",
|
|
106
|
+
"assets": [
|
|
107
|
+
{
|
|
108
|
+
"glob": "*.js",
|
|
109
|
+
"input": "node_modules/@chill-sharp/ui-core/service-worker",
|
|
110
|
+
"output": "/"
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
"glob": "**/*",
|
|
114
|
+
"input": "public"
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
"glob": "**/*",
|
|
118
|
+
"input": "src/assets",
|
|
119
|
+
"output": "assets"
|
|
120
|
+
}
|
|
121
|
+
],
|
|
122
|
+
"styles": [
|
|
123
|
+
"src/styles.scss"
|
|
124
|
+
],
|
|
125
|
+
"scripts": []
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# Current User Preferences
|
|
2
|
+
|
|
3
|
+
Versione italiana: [Italiano](./it/CurrentUserPreferences.md)
|
|
4
|
+
|
|
5
|
+
`ChillSharp.Auth` can make the authenticated user's display preferences available to entity lifecycle hooks without querying `AuthUser` during every save.
|
|
6
|
+
|
|
7
|
+
The immutable `ChillUserPreferences` snapshot contains:
|
|
8
|
+
|
|
9
|
+
- `DisplayCultureName`
|
|
10
|
+
- `DisplayTimeZone`
|
|
11
|
+
- `DisplayDateFormat`
|
|
12
|
+
- `DisplayNumberFormat`
|
|
13
|
+
- `PreferredTheme`
|
|
14
|
+
|
|
15
|
+
## Registration And Cache Lifecycle
|
|
16
|
+
|
|
17
|
+
`AddChillAuthApi<TContext>()` registers a singleton `IChillAuthUserPreferencesCache` and scoped `IChillAuthUserPreferencesAccessor`.
|
|
18
|
+
|
|
19
|
+
When the user logs in or refreshes a token, ChillSharp loads the matching `AuthUser` once and warms a snapshot keyed by its `ExternalId`. The accessor only reads that in-memory snapshot during requests; it does not query the user table.
|
|
20
|
+
|
|
21
|
+
`ChillAuthService` refreshes the cached snapshot after creating or updating an `AuthUser`. It invalidates a deleted user's snapshot and both the previous and new keys when `ExternalId` changes.
|
|
22
|
+
|
|
23
|
+
The cache stores scalar preference values, never a tracked EF Core `AuthUser` instance.
|
|
24
|
+
|
|
25
|
+
## Current User Preferences API
|
|
26
|
+
|
|
27
|
+
Authenticated clients can retrieve the same snapshot used by server-side code with:
|
|
28
|
+
|
|
29
|
+
```http
|
|
30
|
+
GET /api/chill-auth/current-user-preferences
|
|
31
|
+
Authorization: Bearer <access token>
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
The response is a `ChillUserPreferences` JSON object:
|
|
35
|
+
|
|
36
|
+
```json
|
|
37
|
+
{
|
|
38
|
+
"displayCultureName": "it-IT",
|
|
39
|
+
"displayTimeZone": "Europe/Rome",
|
|
40
|
+
"displayDateFormat": "dd/MM/yyyy",
|
|
41
|
+
"displayNumberFormat": "N2",
|
|
42
|
+
"preferredTheme": "cini"
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Use this endpoint after login and when restoring an authenticated UI session. It is the source of truth for the language/culture, time zone, date format, and number format used by the API client and UI; do not derive those values from the browser, operating system, token claims, or an auth-management endpoint.
|
|
47
|
+
|
|
48
|
+
The C# client exposes `GetCurrentUserPreferences()`. The Python client exposes `get_current_user_preferences()`, while the TypeScript client and Angular wrapper expose `getCurrentUserPreferences()`. The Vue and React packages additionally provide `useCurrentUserPreferences()`.
|
|
49
|
+
|
|
50
|
+
In the Angular UI Core package, inject `ChillService` and consume its `userPreferences` signal or the individual `displayCultureName`, `displayTimeZone`, `displayDateFormat`, `displayNumberFormat`, and `preferredTheme` projections. `PreferredTheme` is an opaque server-stored string. UI Core uses browser `prefers-color-scheme` light/dark before login; client apps add selectable themes with `provideChillSharpUiCore({ additionalThemes: ['cini'] })`.
|
|
51
|
+
|
|
52
|
+
## Host DbContext
|
|
53
|
+
|
|
54
|
+
`IChillContext.GetCurrentUserPreferences()` returns `ChillUserPreferences.Empty` by default. In an auth-enabled custom host context, inject `IChillAuthUserPreferencesAccessor` and delegate to it:
|
|
55
|
+
|
|
56
|
+
```csharp
|
|
57
|
+
using ChillSharp;
|
|
58
|
+
using ChillSharp.Auth.Services;
|
|
59
|
+
|
|
60
|
+
public class AppDbContext : DbContext, IChillContext, IChillAuthDbContext
|
|
61
|
+
{
|
|
62
|
+
private readonly IChillAuthUserPreferencesAccessor? _userPreferencesAccessor;
|
|
63
|
+
|
|
64
|
+
public AppDbContext(
|
|
65
|
+
DbContextOptions<AppDbContext> options,
|
|
66
|
+
IChillAuthUserPreferencesAccessor? userPreferencesAccessor = null)
|
|
67
|
+
: base(options)
|
|
68
|
+
{
|
|
69
|
+
_userPreferencesAccessor = userPreferencesAccessor;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
public ChillUserPreferences GetCurrentUserPreferences() =>
|
|
73
|
+
_userPreferencesAccessor?.Current ?? ChillUserPreferences.Empty;
|
|
74
|
+
|
|
75
|
+
public string GetDefaultUserCultureName()
|
|
76
|
+
{
|
|
77
|
+
var cultureName = GetCurrentUserPreferences().DisplayCultureName;
|
|
78
|
+
return string.IsNullOrWhiteSpace(cultureName)
|
|
79
|
+
? GetPrimaryCultureName()
|
|
80
|
+
: cultureName;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
The optional constructor parameter keeps design-time creation and tests that construct the context directly compatible. A normal dependency-injection-created context receives the accessor.
|
|
86
|
+
|
|
87
|
+
`ChillAuthDbContext` already implements this pattern.
|
|
88
|
+
|
|
89
|
+
## Entity Lifecycle Hooks
|
|
90
|
+
|
|
91
|
+
Use the snapshot directly from the supplied `IChillContext`. No database access is required in the hook.
|
|
92
|
+
|
|
93
|
+
```csharp
|
|
94
|
+
public override void OnUpdate(IChillContext context)
|
|
95
|
+
{
|
|
96
|
+
var preferences = context.GetCurrentUserPreferences();
|
|
97
|
+
var timeZone = preferences.DisplayTimeZone;
|
|
98
|
+
|
|
99
|
+
if (!string.IsNullOrWhiteSpace(timeZone))
|
|
100
|
+
{
|
|
101
|
+
// Apply application-specific time-zone behavior.
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
All values can be empty for background jobs, unauthenticated calls, or a user without a matching `AuthUser`. Treat them as optional and choose an application fallback. Validate `DisplayTimeZone` before using it as a time-zone identifier.
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# Preferenze Dell'Utente Corrente
|
|
2
|
+
|
|
3
|
+
English version: [English](../CurrentUserPreferences.md)
|
|
4
|
+
|
|
5
|
+
`ChillSharp.Auth` puo rendere disponibili agli hook del ciclo di vita delle entita le preferenze di visualizzazione dell'utente autenticato senza interrogare `AuthUser` a ogni salvataggio.
|
|
6
|
+
|
|
7
|
+
Lo snapshot immutabile `ChillUserPreferences` contiene:
|
|
8
|
+
|
|
9
|
+
- `DisplayCultureName`
|
|
10
|
+
- `DisplayTimeZone`
|
|
11
|
+
- `DisplayDateFormat`
|
|
12
|
+
- `DisplayNumberFormat`
|
|
13
|
+
- `PreferredTheme`
|
|
14
|
+
|
|
15
|
+
## Registrazione E Ciclo Di Vita Della Cache
|
|
16
|
+
|
|
17
|
+
`AddChillAuthApi<TContext>()` registra il singleton `IChillAuthUserPreferencesCache` e lo scoped `IChillAuthUserPreferencesAccessor`.
|
|
18
|
+
|
|
19
|
+
Quando l'utente esegue il login o rinnova un token, ChillSharp carica una volta l'`AuthUser` corrispondente e inizializza uno snapshot associato al suo `ExternalId`. L'accessor legge solo quello snapshot in memoria durante le richieste; non interroga la tabella utenti.
|
|
20
|
+
|
|
21
|
+
`ChillAuthService` aggiorna lo snapshot in cache dopo la creazione o l'aggiornamento di un `AuthUser`. Invalida lo snapshot di un utente eliminato ed entrambe le chiavi, precedente e nuova, quando cambia `ExternalId`.
|
|
22
|
+
|
|
23
|
+
La cache memorizza valori scalari delle preferenze, mai un'istanza EF Core tracciata di `AuthUser`.
|
|
24
|
+
|
|
25
|
+
## API delle preferenze dell'utente corrente
|
|
26
|
+
|
|
27
|
+
I client autenticati possono recuperare lo stesso snapshot utilizzato dal codice server con:
|
|
28
|
+
|
|
29
|
+
```http
|
|
30
|
+
GET /api/chill-auth/current-user-preferences
|
|
31
|
+
Authorization: Bearer <access token>
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
La risposta e un oggetto JSON `ChillUserPreferences`:
|
|
35
|
+
|
|
36
|
+
```json
|
|
37
|
+
{
|
|
38
|
+
"displayCultureName": "it-IT",
|
|
39
|
+
"displayTimeZone": "Europe/Rome",
|
|
40
|
+
"displayDateFormat": "dd/MM/yyyy",
|
|
41
|
+
"displayNumberFormat": "N2",
|
|
42
|
+
"preferredTheme": "cini"
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Usa questo endpoint dopo il login e durante il ripristino di una sessione UI autenticata. E la fonte autorevole per lingua/cultura, fuso orario, formato data e formato numerico usati dal client API e dalla UI; non ricavare tali valori dal browser, dal sistema operativo, dalle claim del token o da un endpoint di gestione dell'autenticazione.
|
|
47
|
+
|
|
48
|
+
Il client C# espone `GetCurrentUserPreferences()`. Il client Python espone `get_current_user_preferences()`, mentre il client TypeScript e il wrapper Angular espongono `getCurrentUserPreferences()`. I pacchetti Vue e React forniscono inoltre `useCurrentUserPreferences()`.
|
|
49
|
+
|
|
50
|
+
Nel pacchetto Angular UI Core, inietta `ChillService` e usa il signal `userPreferences` oppure le singole proiezioni `displayCultureName`, `displayTimeZone`, `displayDateFormat`, `displayNumberFormat` e `preferredTheme`. `PreferredTheme` e una stringa opaca salvata dal server. UI Core usa `prefers-color-scheme` del browser prima del login; le app client aggiungono temi selezionabili con `provideChillSharpUiCore({ additionalThemes: ['cini'] })`.
|
|
51
|
+
|
|
52
|
+
## DbContext Host
|
|
53
|
+
|
|
54
|
+
Per impostazione predefinita `IChillContext.GetCurrentUserPreferences()` restituisce `ChillUserPreferences.Empty`. In un contesto host personalizzato con auth abilitata, inietta `IChillAuthUserPreferencesAccessor` e delega a esso:
|
|
55
|
+
|
|
56
|
+
```csharp
|
|
57
|
+
using ChillSharp;
|
|
58
|
+
using ChillSharp.Auth.Services;
|
|
59
|
+
|
|
60
|
+
public class AppDbContext : DbContext, IChillContext, IChillAuthDbContext
|
|
61
|
+
{
|
|
62
|
+
private readonly IChillAuthUserPreferencesAccessor? _userPreferencesAccessor;
|
|
63
|
+
|
|
64
|
+
public AppDbContext(
|
|
65
|
+
DbContextOptions<AppDbContext> options,
|
|
66
|
+
IChillAuthUserPreferencesAccessor? userPreferencesAccessor = null)
|
|
67
|
+
: base(options)
|
|
68
|
+
{
|
|
69
|
+
_userPreferencesAccessor = userPreferencesAccessor;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
public ChillUserPreferences GetCurrentUserPreferences() =>
|
|
73
|
+
_userPreferencesAccessor?.Current ?? ChillUserPreferences.Empty;
|
|
74
|
+
|
|
75
|
+
public string GetDefaultUserCultureName()
|
|
76
|
+
{
|
|
77
|
+
var cultureName = GetCurrentUserPreferences().DisplayCultureName;
|
|
78
|
+
return string.IsNullOrWhiteSpace(cultureName)
|
|
79
|
+
? GetPrimaryCultureName()
|
|
80
|
+
: cultureName;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Il parametro opzionale del costruttore mantiene compatibile la creazione a design-time e i test che costruiscono direttamente il contesto. Un contesto creato normalmente dalla dependency injection riceve l'accessor.
|
|
86
|
+
|
|
87
|
+
`ChillAuthDbContext` implementa gia questo schema.
|
|
88
|
+
|
|
89
|
+
## Hook Del Ciclo Di Vita Delle Entita
|
|
90
|
+
|
|
91
|
+
Usa lo snapshot direttamente dall'`IChillContext` fornito. Nell'hook non e necessario alcun accesso al database.
|
|
92
|
+
|
|
93
|
+
```csharp
|
|
94
|
+
public override void OnUpdate(IChillContext context)
|
|
95
|
+
{
|
|
96
|
+
var preferences = context.GetCurrentUserPreferences();
|
|
97
|
+
var timeZone = preferences.DisplayTimeZone;
|
|
98
|
+
|
|
99
|
+
if (!string.IsNullOrWhiteSpace(timeZone))
|
|
100
|
+
{
|
|
101
|
+
// Applica il comportamento relativo al fuso orario dell'applicazione.
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Tutti i valori possono essere vuoti per job in background, chiamate non autenticate o un utente senza un `AuthUser` corrispondente. Considerali opzionali e scegli un fallback dell'applicazione. Valida `DisplayTimeZone` prima di usarlo come identificatore di fuso orario.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "chill-sharp-ui-template",
|
|
3
|
+
"version": "1.1.6",
|
|
4
|
+
"private": true,
|
|
5
|
+
"description": "Template Angular shell for ChillSharp UI client applications.",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"ng": "ng",
|
|
8
|
+
"start": "ng serve",
|
|
9
|
+
"build": "ng build",
|
|
10
|
+
"watch": "ng build --watch --configuration development",
|
|
11
|
+
"test": "ng test --watch=false",
|
|
12
|
+
"lint": "tsc --noEmit -p tsconfig.app.json"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@angular/cdk": "^19.2.19",
|
|
16
|
+
"@angular/common": "^19.2.0",
|
|
17
|
+
"@angular/compiler": "^19.2.0",
|
|
18
|
+
"@angular/core": "^19.2.0",
|
|
19
|
+
"@angular/forms": "^19.2.0",
|
|
20
|
+
"@angular/platform-browser": "^19.2.0",
|
|
21
|
+
"@angular/platform-browser-dynamic": "^19.2.0",
|
|
22
|
+
"@angular/router": "^19.2.0",
|
|
23
|
+
"@chill-sharp/ng-client": "file:packages/chill-sharp-ng-client-1.1.6.tgz",
|
|
24
|
+
"@chill-sharp/ts-client": "file:packages/chill-sharp-ts-client-1.1.6.tgz",
|
|
25
|
+
"@chill-sharp/ui-core": "file:packages/chill-sharp-ui-core-1.1.6.tgz",
|
|
26
|
+
"rxjs": "~7.8.0",
|
|
27
|
+
"tslib": "^2.3.0",
|
|
28
|
+
"zone.js": "~0.15.0"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@angular-devkit/build-angular": "^19.2.20",
|
|
32
|
+
"@angular/cli": "^19.2.20",
|
|
33
|
+
"@angular/compiler-cli": "^19.2.20",
|
|
34
|
+
"@types/jasmine": "~5.1.0",
|
|
35
|
+
"jasmine-core": "~5.6.0",
|
|
36
|
+
"karma": "~6.4.0",
|
|
37
|
+
"karma-chrome-launcher": "~3.2.0",
|
|
38
|
+
"karma-coverage": "~2.2.0",
|
|
39
|
+
"karma-jasmine": "~5.1.0",
|
|
40
|
+
"karma-jasmine-html-reporter": "~2.1.0",
|
|
41
|
+
"typescript": "~5.7.2"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
(function () {
|
|
2
|
+
const DEBUG_CHILLSHARP_ENV = {
|
|
3
|
+
uiUrl: 'http://localhost:6202',
|
|
4
|
+
apiUrl: 'https://localhost:6002/api'
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
function readEnvValue(value, fallback) {
|
|
8
|
+
return value && !/^\$\{[^}]+\}$/.test(value)
|
|
9
|
+
? value
|
|
10
|
+
: fallback;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
globalThis.CHILLSHARP_UI_URL = readEnvValue('${CHILLSHARP_UI_URL}', DEBUG_CHILLSHARP_ENV.uiUrl);
|
|
14
|
+
globalThis.CHILLSHARP_API_URL = readEnvValue('${CHILLSHARP_API_URL}', DEBUG_CHILLSHARP_ENV.apiUrl);
|
|
15
|
+
}());
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
globalThis.__clientUiTemplateRuntimeConfig__ = globalThis.__clientUiTemplateRuntimeConfig__ ?? {
|
|
2
|
+
tenantCode: 'template-client',
|
|
3
|
+
featureFlags: {
|
|
4
|
+
clientHomeEnabled: true
|
|
5
|
+
}
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
globalThis.__chillSharpUiRuntimeConfig__ = globalThis.__chillSharpUiRuntimeConfig__ ?? {
|
|
9
|
+
workspaceTaskSources: []
|
|
10
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Shared cache behavior is upgraded with @chill-sharp/ui-core.
|
|
2
|
+
self.CHILL_SHARP_SW_OPTIONS = {
|
|
3
|
+
cachePrefix: 'chill-sharp-ui',
|
|
4
|
+
cacheVersion: 'v2',
|
|
5
|
+
cacheTimeoutMs: 10 * 60 * 1000,
|
|
6
|
+
appShell: ['/', '/index.html']
|
|
7
|
+
};
|
|
8
|
+
importScripts('./chill-sharp-service-worker.js');
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { TestBed } from '@angular/core/testing';
|
|
2
|
+
import { AppComponent } from './app.component';
|
|
3
|
+
|
|
4
|
+
describe('AppComponent', () => {
|
|
5
|
+
beforeEach(async () => {
|
|
6
|
+
await TestBed.configureTestingModule({
|
|
7
|
+
imports: [AppComponent]
|
|
8
|
+
}).compileComponents();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('creates the shell component', () => {
|
|
12
|
+
const fixture = TestBed.createComponent(AppComponent);
|
|
13
|
+
expect(fixture.componentInstance).toBeTruthy();
|
|
14
|
+
});
|
|
15
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Component } from '@angular/core';
|
|
2
|
+
import { ChillSharpUiRootComponent } from '@chill-sharp/ui-core';
|
|
3
|
+
|
|
4
|
+
@Component({
|
|
5
|
+
selector: 'app-root',
|
|
6
|
+
standalone: true,
|
|
7
|
+
imports: [ChillSharpUiRootComponent],
|
|
8
|
+
template: '<chill-sharp-ui-root />'
|
|
9
|
+
})
|
|
10
|
+
export class AppComponent {}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { APP_INITIALIZER, ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
|
|
2
|
+
import { provideRouter } from '@angular/router';
|
|
3
|
+
import { provideChillSharpUiCore } from '@chill-sharp/ui-core';
|
|
4
|
+
import { appRoutes } from './app.routes';
|
|
5
|
+
import { CLIENT_APP_CONFIG } from '../config/app-config';
|
|
6
|
+
import { provideClientTemplateProviders } from './core/providers/client-template.providers';
|
|
7
|
+
|
|
8
|
+
export const appConfig: ApplicationConfig = {
|
|
9
|
+
providers: [
|
|
10
|
+
provideZoneChangeDetection({ eventCoalescing: true }),
|
|
11
|
+
provideRouter(appRoutes),
|
|
12
|
+
...provideChillSharpUiCore(),
|
|
13
|
+
...provideClientTemplateProviders(),
|
|
14
|
+
{
|
|
15
|
+
provide: APP_INITIALIZER,
|
|
16
|
+
multi: true,
|
|
17
|
+
useFactory: () => () => {
|
|
18
|
+
globalThis.document.title = CLIENT_APP_CONFIG.appName;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
]
|
|
22
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Routes } from '@angular/router';
|
|
2
|
+
import { CHILL_SHARP_UI_ROUTES } from '@chill-sharp/ui-core';
|
|
3
|
+
import { getClientFeatureRoutes } from './core/plugins/register-client-plugins';
|
|
4
|
+
|
|
5
|
+
const coreRoutes = CHILL_SHARP_UI_ROUTES.filter((route) => route.path !== '**');
|
|
6
|
+
|
|
7
|
+
export const appRoutes: Routes = [
|
|
8
|
+
...getClientFeatureRoutes(),
|
|
9
|
+
...coreRoutes,
|
|
10
|
+
{
|
|
11
|
+
path: '**',
|
|
12
|
+
redirectTo: 'login'
|
|
13
|
+
}
|
|
14
|
+
];
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Client Overrides
|
|
2
|
+
|
|
3
|
+
Use this folder for client-specific provider replacements and future override registrations exposed by `@chill-sharp/ui-core`.
|
|
4
|
+
|
|
5
|
+
Typical examples:
|
|
6
|
+
|
|
7
|
+
- branded header providers
|
|
8
|
+
- custom page title strategy
|
|
9
|
+
- specialized service implementations
|
|
10
|
+
- feature-flag-driven replacements
|
|
11
|
+
|
|
12
|
+
`register-client-overrides.ts` returns an empty provider array by default so each client repo can opt into overrides intentionally.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Client Plugins
|
|
2
|
+
|
|
3
|
+
Use this folder for client-owned extension registration.
|
|
4
|
+
|
|
5
|
+
Typical examples:
|
|
6
|
+
|
|
7
|
+
- client-only routes
|
|
8
|
+
- extra menu integration when the core exposes plugin hooks
|
|
9
|
+
- additional providers required by local features
|
|
10
|
+
|
|
11
|
+
`register-client-plugins.ts` currently demonstrates the simplest template-owned extension: a client route.
|
|
12
|
+
|
|
13
|
+
Included examples:
|
|
14
|
+
|
|
15
|
+
- [`hello-plugin`](hello-plugin/README.md): route plugin that renders `Hello {{name}}` from `hello-plugin/:name`
|
|
16
|
+
|
|
17
|
+
For plugin README guidance, see [`../../../../../HOW_TO_CREATE_UI_CORE_PLUGIN_README.md`](../../../../../HOW_TO_CREATE_UI_CORE_PLUGIN_README.md).
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Hello Plugin
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
This is a simple template-owned route plugin. It renders:
|
|
6
|
+
|
|
7
|
+
```text
|
|
8
|
+
Hello {{name}}
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
where `name` is read from the route parameter.
|
|
12
|
+
|
|
13
|
+
## Registration
|
|
14
|
+
|
|
15
|
+
The plugin is registered by `src/app/core/plugins/register-client-plugins.ts` at:
|
|
16
|
+
|
|
17
|
+
```text
|
|
18
|
+
hello-plugin/:name
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Example URL:
|
|
22
|
+
|
|
23
|
+
```text
|
|
24
|
+
/hello-plugin/Manu
|
|
25
|
+
```
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { CommonModule } from '@angular/common';
|
|
2
|
+
import { Component, computed, inject } from '@angular/core';
|
|
3
|
+
import { toSignal } from '@angular/core/rxjs-interop';
|
|
4
|
+
import { ActivatedRoute } from '@angular/router';
|
|
5
|
+
import { map } from 'rxjs';
|
|
6
|
+
|
|
7
|
+
@Component({
|
|
8
|
+
selector: 'app-hello-plugin',
|
|
9
|
+
standalone: true,
|
|
10
|
+
imports: [CommonModule],
|
|
11
|
+
template: `
|
|
12
|
+
<section class="client-template-page">
|
|
13
|
+
<p class="eyebrow">Hello Plugin</p>
|
|
14
|
+
<h1>Hello {{ name() }}</h1>
|
|
15
|
+
</section>
|
|
16
|
+
`
|
|
17
|
+
})
|
|
18
|
+
export class HelloPluginComponent {
|
|
19
|
+
private readonly route = inject(ActivatedRoute);
|
|
20
|
+
private readonly routeName = toSignal(this.route.paramMap.pipe(
|
|
21
|
+
map((params) => params.get('name')?.trim() ?? '')
|
|
22
|
+
), { initialValue: '' });
|
|
23
|
+
|
|
24
|
+
protected readonly name = computed(() => {
|
|
25
|
+
const value = this.routeName();
|
|
26
|
+
return value || 'World';
|
|
27
|
+
});
|
|
28
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Routes } from '@angular/router';
|
|
2
|
+
import { HelloPluginComponent } from './hello-plugin/hello-plugin.component';
|
|
3
|
+
import { ClientHomeComponent } from '../../pages/client-home/client-home.component';
|
|
4
|
+
|
|
5
|
+
export function getClientFeatureRoutes(): Routes {
|
|
6
|
+
return [
|
|
7
|
+
{
|
|
8
|
+
path: 'hello-plugin/:name',
|
|
9
|
+
component: HelloPluginComponent
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
path: 'client-home',
|
|
13
|
+
component: ClientHomeComponent
|
|
14
|
+
}
|
|
15
|
+
];
|
|
16
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { CommonModule } from '@angular/common';
|
|
2
|
+
import { Component } from '@angular/core';
|
|
3
|
+
import { CLIENT_APP_CONFIG } from '../../../config/app-config';
|
|
4
|
+
import { readClientRuntimeConfig } from '../../../config/runtime-config';
|
|
5
|
+
|
|
6
|
+
@Component({
|
|
7
|
+
selector: 'app-client-home',
|
|
8
|
+
standalone: true,
|
|
9
|
+
imports: [CommonModule],
|
|
10
|
+
template: `
|
|
11
|
+
<section class="client-template-page">
|
|
12
|
+
<p class="eyebrow">Client Template</p>
|
|
13
|
+
<h1>{{ appConfig.appName }}</h1>
|
|
14
|
+
<p>
|
|
15
|
+
This is a client-owned route that lives in the template shell instead of the shared
|
|
16
|
+
<code>@chill-sharp/ui-core</code> package.
|
|
17
|
+
</p>
|
|
18
|
+
|
|
19
|
+
<div class="client-template-grid">
|
|
20
|
+
<article class="client-template-card">
|
|
21
|
+
<strong>Tenant</strong>
|
|
22
|
+
<p>{{ runtimeConfig.tenantCode }}</p>
|
|
23
|
+
</article>
|
|
24
|
+
|
|
25
|
+
<article class="client-template-card">
|
|
26
|
+
<strong>API</strong>
|
|
27
|
+
<p>{{ appConfig.apiBaseUrl }}</p>
|
|
28
|
+
</article>
|
|
29
|
+
|
|
30
|
+
<article class="client-template-card">
|
|
31
|
+
<strong>Theme</strong>
|
|
32
|
+
<p>{{ appConfig.themeName }}</p>
|
|
33
|
+
</article>
|
|
34
|
+
</div>
|
|
35
|
+
</section>
|
|
36
|
+
`
|
|
37
|
+
})
|
|
38
|
+
export class ClientHomeComponent {
|
|
39
|
+
protected readonly appConfig = CLIENT_APP_CONFIG;
|
|
40
|
+
protected readonly runtimeConfig = readClientRuntimeConfig();
|
|
41
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 160 48" role="img" aria-labelledby="title">
|
|
2
|
+
<title>Client Template Logo</title>
|
|
3
|
+
<rect width="160" height="48" rx="12" fill="#13424d"/>
|
|
4
|
+
<circle cx="28" cy="24" r="10" fill="#f5c46b"/>
|
|
5
|
+
<path d="M52 16h54c8 0 14 6 14 14s-6 14-14 14H52z" fill="#0f6d61"/>
|
|
6
|
+
<text x="60" y="30" font-family="Segoe UI, sans-serif" font-size="15" fill="#fffdf8">Client UI</text>
|
|
7
|
+
</svg>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { environment } from '../environments/environment';
|
|
2
|
+
|
|
3
|
+
export interface ClientAppConfig {
|
|
4
|
+
appName: string;
|
|
5
|
+
apiBaseUrl: string;
|
|
6
|
+
tenantCode: string;
|
|
7
|
+
themeName: string;
|
|
8
|
+
supportEmail: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const CLIENT_APP_CONFIG: ClientAppConfig = {
|
|
12
|
+
appName: 'ChillSharp Client Template',
|
|
13
|
+
apiBaseUrl: environment.apiBaseUrl,
|
|
14
|
+
tenantCode: 'template-client',
|
|
15
|
+
themeName: 'template-default',
|
|
16
|
+
supportEmail: 'support@example.com'
|
|
17
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface ClientTemplateRuntimeConfig {
|
|
2
|
+
tenantCode: string;
|
|
3
|
+
featureFlags: Record<string, boolean>;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function readClientRuntimeConfig(): ClientTemplateRuntimeConfig {
|
|
7
|
+
const runtimeConfig = globalThis.__clientUiTemplateRuntimeConfig__;
|
|
8
|
+
|
|
9
|
+
return {
|
|
10
|
+
tenantCode: runtimeConfig?.tenantCode ?? 'template-client',
|
|
11
|
+
featureFlags: runtimeConfig?.featureFlags ?? {}
|
|
12
|
+
};
|
|
13
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<title>ChillSharp UI Template</title>
|
|
6
|
+
<base href="/">
|
|
7
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<script src="env.js"></script>
|
|
11
|
+
<script src="runtime-config.js"></script>
|
|
12
|
+
<app-root></app-root>
|
|
13
|
+
<script>
|
|
14
|
+
if ('serviceWorker' in navigator) {
|
|
15
|
+
window.addEventListener('load', () => {
|
|
16
|
+
navigator.serviceWorker.register('/sw.js', { updateViaCache: 'none' })
|
|
17
|
+
.catch((error) => console.error('Service worker registration failed.', error));
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
</script>
|
|
21
|
+
</body>
|
|
22
|
+
</html>
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
@use '@chill-sharp/ui-core/styles/core-theme.scss';
|
|
2
|
+
|
|
3
|
+
:root {
|
|
4
|
+
--font-sans: "Segoe UI Variable", "Segoe UI", sans-serif;
|
|
5
|
+
--bg-top: #f6f0e8;
|
|
6
|
+
--bg-bottom: #dfeaea;
|
|
7
|
+
--accent: #0f6d61;
|
|
8
|
+
--accent-strong: #13424d;
|
|
9
|
+
--accent-soft: rgba(15, 109, 97, 0.14);
|
|
10
|
+
--surface-0: #fffdf8;
|
|
11
|
+
--surface-1: rgba(255, 253, 248, 0.84);
|
|
12
|
+
--surface-2: rgba(255, 255, 255, 0.72);
|
|
13
|
+
--surface-3: rgba(239, 246, 246, 0.9);
|
|
14
|
+
--text-main: #18313b;
|
|
15
|
+
--text-muted: #64757a;
|
|
16
|
+
--border-color: rgba(24, 49, 59, 0.13);
|
|
17
|
+
--shadow: 0 24px 60px rgba(20, 65, 77, 0.12);
|
|
18
|
+
--shadow-soft: 0 12px 30px rgba(20, 65, 77, 0.08);
|
|
19
|
+
--workspace-backdrop:
|
|
20
|
+
radial-gradient(circle at top left, rgba(15, 109, 97, 0.16), transparent 28%),
|
|
21
|
+
radial-gradient(circle at top right, rgba(188, 146, 93, 0.14), transparent 24%),
|
|
22
|
+
linear-gradient(180deg, var(--bg-top), var(--bg-bottom));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
body::before {
|
|
26
|
+
content: '';
|
|
27
|
+
position: fixed;
|
|
28
|
+
inset: 0;
|
|
29
|
+
pointer-events: none;
|
|
30
|
+
background:
|
|
31
|
+
linear-gradient(90deg, rgba(255, 255, 255, 0.12) 1px, transparent 1px),
|
|
32
|
+
linear-gradient(rgba(255, 255, 255, 0.12) 1px, transparent 1px);
|
|
33
|
+
background-size: 2rem 2rem;
|
|
34
|
+
mask-image: linear-gradient(180deg, rgba(0, 0, 0, 0.3), transparent 70%);
|
|
35
|
+
opacity: 0.28;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.client-template-page {
|
|
39
|
+
max-width: 64rem;
|
|
40
|
+
margin: 2.5rem auto;
|
|
41
|
+
padding: 2rem;
|
|
42
|
+
border-radius: 1.5rem;
|
|
43
|
+
border: 1px solid var(--border-color);
|
|
44
|
+
background: var(--surface-1);
|
|
45
|
+
box-shadow: var(--shadow-soft);
|
|
46
|
+
backdrop-filter: blur(16px);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.client-template-page h1 {
|
|
50
|
+
margin: 0;
|
|
51
|
+
font-size: clamp(2rem, 5vw, 3.4rem);
|
|
52
|
+
line-height: 0.95;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.client-template-page p {
|
|
56
|
+
color: var(--text-muted);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.client-template-grid {
|
|
60
|
+
display: grid;
|
|
61
|
+
grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
|
|
62
|
+
gap: 1rem;
|
|
63
|
+
margin-top: 1.5rem;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.client-template-card {
|
|
67
|
+
padding: 1rem;
|
|
68
|
+
border-radius: 1rem;
|
|
69
|
+
background: var(--surface-2);
|
|
70
|
+
border: 1px solid var(--border-color);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Keep the application menu dense and list-like instead of presenting every
|
|
74
|
+
// entry as a separate card. These selectors intentionally override the
|
|
75
|
+
// public UI-core menu classes from the client template.
|
|
76
|
+
.workspace-menu__tree {
|
|
77
|
+
gap: 0.15rem;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.workspace-menu__tree-node,
|
|
81
|
+
.workspace-menu__tree-children {
|
|
82
|
+
gap: 0.1rem;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.workspace-menu__tree-row {
|
|
86
|
+
gap: 0.25rem;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.workspace-menu__tree-main {
|
|
90
|
+
border: 0;
|
|
91
|
+
border-radius: 0.55rem;
|
|
92
|
+
background: transparent;
|
|
93
|
+
box-shadow: none;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.workspace-menu__tree-main.is-active {
|
|
97
|
+
border: 0;
|
|
98
|
+
background: var(--accent-soft);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.workspace-menu__tree-main.is-pending-expand {
|
|
102
|
+
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 42%, transparent);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.workspace-menu__tree-trigger {
|
|
106
|
+
padding: 0.5rem 0.7rem;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.workspace-menu__tree-expander {
|
|
110
|
+
width: 2.25rem;
|
|
111
|
+
padding: 0.45rem;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.workspace-menu__tree-meta {
|
|
115
|
+
padding: 0 0.7rem 0.5rem;
|
|
116
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compileOnSave": false,
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"outDir": "./dist/out-tsc",
|
|
5
|
+
"strict": true,
|
|
6
|
+
"noImplicitOverride": true,
|
|
7
|
+
"noPropertyAccessFromIndexSignature": true,
|
|
8
|
+
"noImplicitReturns": true,
|
|
9
|
+
"noFallthroughCasesInSwitch": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"isolatedModules": true,
|
|
12
|
+
"esModuleInterop": true,
|
|
13
|
+
"experimentalDecorators": true,
|
|
14
|
+
"importHelpers": true,
|
|
15
|
+
"target": "ES2022",
|
|
16
|
+
"module": "preserve"
|
|
17
|
+
},
|
|
18
|
+
"angularCompilerOptions": {
|
|
19
|
+
"enableI18nLegacyMessageIdFormat": false,
|
|
20
|
+
"strictInjectionParameters": true,
|
|
21
|
+
"strictInputAccessModifiers": true,
|
|
22
|
+
"strictTemplates": true
|
|
23
|
+
},
|
|
24
|
+
"files": [],
|
|
25
|
+
"references": [
|
|
26
|
+
{
|
|
27
|
+
"path": "./tsconfig.app.json"
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"path": "./tsconfig.spec.json"
|
|
31
|
+
}
|
|
32
|
+
]
|
|
33
|
+
}
|