@salesforce/templates 66.11.0 → 66.11.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/lib/templates/project/reactexternalapp/_p_/_m_/_w_/_a_/package-lock.json +361 -332
- package/lib/templates/project/reactinternalapp/_p_/_m_/_w_/_a_/package-lock.json +354 -325
- package/lib/templates/uiBundles/angularbasic/angular.json +11 -7
- package/lib/templates/uiBundles/angularbasic/eslint.config.js +4 -0
- package/lib/templates/uiBundles/angularbasic/package-lock.json +292 -239
- package/lib/templates/uiBundles/angularbasic/package.json +4 -0
- package/lib/templates/uiBundles/angularbasic/src/app/api/{graphql-client.service.spec.ts → data-client.service.spec.ts} +36 -6
- package/lib/templates/uiBundles/angularbasic/src/app/api/data-client.service.ts +74 -0
- package/lib/templates/uiBundles/angularbasic/src/app/app.config.ts +15 -1
- package/lib/templates/uiBundles/angularbasic/src/app/app.routes.ts +6 -1
- package/lib/templates/uiBundles/angularbasic/src/app/app.ts +7 -2
- package/lib/templates/uiBundles/angularbasic/src/app/{layout → components/layout/app-layout}/app-layout.component.html +2 -1
- package/lib/templates/uiBundles/angularbasic/src/app/{layout → components/layout/app-layout}/app-layout.component.ts +9 -3
- package/lib/templates/uiBundles/angularbasic/src/app/components/ui/_field-size.scss +42 -0
- package/lib/templates/uiBundles/angularbasic/src/app/components/ui/alert/alert.component.html +31 -0
- package/lib/templates/uiBundles/angularbasic/src/app/components/ui/alert/alert.component.ts +53 -0
- package/lib/templates/uiBundles/angularbasic/src/app/components/ui/button/button.component.html +18 -0
- package/lib/templates/uiBundles/angularbasic/src/app/components/ui/button/button.component.scss +112 -0
- package/lib/templates/uiBundles/angularbasic/src/app/components/ui/button/button.component.ts +57 -0
- package/lib/templates/uiBundles/angularbasic/src/app/components/ui/card/card.component.scss +34 -0
- package/lib/templates/uiBundles/angularbasic/src/app/components/ui/card/card.component.ts +110 -0
- package/lib/templates/uiBundles/angularbasic/src/app/components/ui/collapsible/collapsible.component.html +10 -0
- package/lib/templates/uiBundles/angularbasic/src/app/components/ui/collapsible/collapsible.component.ts +26 -0
- package/lib/templates/uiBundles/angularbasic/src/app/components/ui/date-picker/date-picker.component.html +25 -0
- package/lib/templates/uiBundles/angularbasic/src/app/components/ui/date-picker/date-picker.component.scss +36 -0
- package/lib/templates/uiBundles/angularbasic/src/app/components/ui/date-picker/date-picker.component.ts +41 -0
- package/lib/templates/uiBundles/angularbasic/src/app/components/ui/date-range-picker/date-range-picker.component.html +34 -0
- package/lib/templates/uiBundles/angularbasic/src/app/components/ui/date-range-picker/date-range-picker.component.scss +33 -0
- package/lib/templates/uiBundles/angularbasic/src/app/components/ui/date-range-picker/date-range-picker.component.ts +66 -0
- package/lib/templates/uiBundles/angularbasic/src/app/components/ui/dialog/dialog.component.html +37 -0
- package/lib/templates/uiBundles/angularbasic/src/app/components/ui/dialog/dialog.component.ts +55 -0
- package/lib/templates/uiBundles/angularbasic/src/app/components/ui/field/field.component.html +21 -0
- package/lib/templates/uiBundles/angularbasic/src/app/components/ui/field/field.component.ts +36 -0
- package/lib/templates/uiBundles/angularbasic/src/app/components/ui/field-size.ts +8 -0
- package/lib/templates/uiBundles/angularbasic/src/app/components/ui/input/input.component.html +19 -0
- package/lib/templates/uiBundles/angularbasic/src/app/components/ui/input/input.component.scss +35 -0
- package/lib/templates/uiBundles/angularbasic/src/app/components/ui/input/input.component.ts +36 -0
- package/lib/templates/uiBundles/angularbasic/src/app/components/ui/label/label.component.html +3 -0
- package/lib/templates/uiBundles/angularbasic/src/app/components/ui/label/label.component.ts +17 -0
- package/lib/templates/uiBundles/angularbasic/src/app/components/ui/paginator/paginator.component.html +10 -0
- package/lib/templates/uiBundles/angularbasic/src/app/components/ui/paginator/paginator.component.ts +37 -0
- package/lib/templates/uiBundles/angularbasic/src/app/components/ui/popover/popover.component.html +41 -0
- package/lib/templates/uiBundles/angularbasic/src/app/components/ui/popover/popover.component.ts +40 -0
- package/lib/templates/uiBundles/angularbasic/src/app/components/ui/select/select.component.html +24 -0
- package/lib/templates/uiBundles/angularbasic/src/app/components/ui/select/select.component.scss +35 -0
- package/lib/templates/uiBundles/angularbasic/src/app/components/ui/select/select.component.ts +72 -0
- package/lib/templates/uiBundles/angularbasic/src/app/components/ui/separator/separator.component.html +1 -0
- package/lib/templates/uiBundles/angularbasic/src/app/components/ui/separator/separator.component.scss +23 -0
- package/lib/templates/uiBundles/angularbasic/src/app/components/ui/separator/separator.component.ts +56 -0
- package/lib/templates/uiBundles/angularbasic/src/app/components/ui/skeleton/skeleton.component.html +1 -0
- package/lib/templates/uiBundles/angularbasic/src/app/components/ui/skeleton/skeleton.component.scss +28 -0
- package/lib/templates/uiBundles/angularbasic/src/app/components/ui/skeleton/skeleton.component.ts +24 -0
- package/lib/templates/uiBundles/angularbasic/src/app/components/ui/spinner/spinner.component.ts +35 -0
- package/lib/templates/uiBundles/angularbasic/src/app/pages/home/home.component.ts +7 -1
- package/lib/templates/uiBundles/angularbasic/src/app/pages/not-found/not-found.component.ts +7 -1
- package/lib/templates/uiBundles/angularbasic/src/app/utils/async-data.ts +99 -0
- package/lib/templates/uiBundles/angularbasic/src/index.html +1 -0
- package/lib/templates/uiBundles/angularbasic/src/styles.scss +170 -0
- package/lib/templates/uiBundles/angularbasic/src/theme.scss +39 -0
- package/lib/templates/uiBundles/reactbasic/package-lock.json +354 -325
- package/package.json +3 -3
- package/lib/templates/uiBundles/angularbasic/src/app/api/graphql-client.service.ts +0 -48
- package/lib/templates/uiBundles/angularbasic/src/app/app.css +0 -0
- package/lib/templates/uiBundles/angularbasic/src/styles.css +0 -12
- /package/lib/templates/uiBundles/angularbasic/src/app/{layout → components/layout/app-layout}/app-layout.component.spec.ts +0 -0
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"dev": "sf-angular-serve",
|
|
11
|
+
"dev:design": "sf-angular-serve",
|
|
11
12
|
"build": "ng build",
|
|
12
13
|
"watch": "ng build --watch --configuration development",
|
|
13
14
|
"test": "ng test",
|
|
@@ -16,10 +17,13 @@
|
|
|
16
17
|
"e2e": "playwright test"
|
|
17
18
|
},
|
|
18
19
|
"dependencies": {
|
|
20
|
+
"@angular/animations": "^21.2.0",
|
|
21
|
+
"@angular/cdk": "^21.2.0",
|
|
19
22
|
"@angular/common": "^21.2.0",
|
|
20
23
|
"@angular/compiler": "^21.2.0",
|
|
21
24
|
"@angular/core": "^21.2.0",
|
|
22
25
|
"@angular/forms": "^21.2.0",
|
|
26
|
+
"@angular/material": "^21.2.0",
|
|
23
27
|
"@angular/platform-browser": "^21.2.0",
|
|
24
28
|
"@angular/router": "^21.2.0",
|
|
25
29
|
"@salesforce/angular-plugin-ui-bundle": "^11.4.4",
|
|
@@ -1,32 +1,35 @@
|
|
|
1
1
|
import { TestBed } from '@angular/core/testing';
|
|
2
|
-
import {
|
|
2
|
+
import { DataClient } from './data-client.service';
|
|
3
3
|
import { createDataSDK } from '@salesforce/platform-sdk';
|
|
4
4
|
|
|
5
5
|
vi.mock('@salesforce/platform-sdk', () => ({
|
|
6
6
|
createDataSDK: vi.fn(),
|
|
7
7
|
}));
|
|
8
8
|
|
|
9
|
-
describe('
|
|
10
|
-
let service:
|
|
9
|
+
describe('DataClient', () => {
|
|
10
|
+
let service: DataClient;
|
|
11
11
|
let mockGraphqlQuery: ReturnType<typeof vi.fn>;
|
|
12
12
|
let mockGraphqlMutate: ReturnType<typeof vi.fn>;
|
|
13
|
+
let mockFetch: ReturnType<typeof vi.fn>;
|
|
13
14
|
|
|
14
15
|
beforeEach(async () => {
|
|
15
16
|
mockGraphqlQuery = vi.fn();
|
|
16
17
|
mockGraphqlMutate = vi.fn();
|
|
18
|
+
mockFetch = vi.fn();
|
|
17
19
|
|
|
18
20
|
vi.mocked(createDataSDK).mockResolvedValue({
|
|
19
21
|
graphql: {
|
|
20
22
|
query: mockGraphqlQuery,
|
|
21
23
|
mutate: mockGraphqlMutate,
|
|
22
24
|
},
|
|
25
|
+
fetch: mockFetch,
|
|
23
26
|
} as any);
|
|
24
27
|
|
|
25
28
|
await TestBed.configureTestingModule({
|
|
26
|
-
providers: [
|
|
29
|
+
providers: [DataClient],
|
|
27
30
|
}).compileComponents();
|
|
28
31
|
|
|
29
|
-
service = TestBed.inject(
|
|
32
|
+
service = TestBed.inject(DataClient);
|
|
30
33
|
});
|
|
31
34
|
|
|
32
35
|
afterEach(() => {
|
|
@@ -35,7 +38,7 @@ describe('GraphqlClient', () => {
|
|
|
35
38
|
|
|
36
39
|
it('should create the service', () => {
|
|
37
40
|
expect(service).toBeTruthy();
|
|
38
|
-
expect(service).toBeInstanceOf(
|
|
41
|
+
expect(service).toBeInstanceOf(DataClient);
|
|
39
42
|
});
|
|
40
43
|
|
|
41
44
|
it('should route plain query operations to graphql.query and return result.data', async () => {
|
|
@@ -151,4 +154,31 @@ describe('GraphqlClient', () => {
|
|
|
151
154
|
expect(mockGraphqlMutate).toHaveBeenCalledWith({ mutation: operation, variables });
|
|
152
155
|
expect(result).toEqual(expectedData);
|
|
153
156
|
});
|
|
157
|
+
|
|
158
|
+
it('should proxy fetch to sdk.fetch and return the raw Response', async () => {
|
|
159
|
+
const url = '/services/apexrest/auth/login';
|
|
160
|
+
const init: RequestInit = {
|
|
161
|
+
method: 'POST',
|
|
162
|
+
body: JSON.stringify({ email: 'a@b.com' }),
|
|
163
|
+
headers: { 'Content-Type': 'application/json' },
|
|
164
|
+
};
|
|
165
|
+
const expectedResponse = { ok: true, status: 200 } as Response;
|
|
166
|
+
mockFetch.mockResolvedValue(expectedResponse);
|
|
167
|
+
|
|
168
|
+
const response = await service.fetch(url, init);
|
|
169
|
+
|
|
170
|
+
expect(mockFetch).toHaveBeenCalledWith(url, init);
|
|
171
|
+
expect(response).toBe(expectedResponse);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('should call fetch with no init when omitted', async () => {
|
|
175
|
+
const url = '/services/data/v64.0/chatter/users/me';
|
|
176
|
+
const expectedResponse = { ok: true, status: 200 } as Response;
|
|
177
|
+
mockFetch.mockResolvedValue(expectedResponse);
|
|
178
|
+
|
|
179
|
+
const response = await service.fetch(url);
|
|
180
|
+
|
|
181
|
+
expect(mockFetch).toHaveBeenCalledWith(url, undefined);
|
|
182
|
+
expect(response).toBe(expectedResponse);
|
|
183
|
+
});
|
|
154
184
|
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { Injectable } from '@angular/core';
|
|
2
|
+
import { createDataSDK } from '@salesforce/platform-sdk';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* True when the operation's first definition is a `mutation`. Strips GraphQL
|
|
6
|
+
* comments first so a leading `# ...` line can't mask the keyword. Queries
|
|
7
|
+
* (named or anonymous `{ ... }` shorthand) and subscriptions fall through to
|
|
8
|
+
* query().
|
|
9
|
+
*/
|
|
10
|
+
function isMutation(operation: string): boolean {
|
|
11
|
+
return /^\s*mutation\b/.test(operation.replace(/#[^\n\r]*/g, ''));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The single data-access client for the app — a thin wrapper over
|
|
16
|
+
* `createDataSDK` that centralizes both of the SDK's surfaces:
|
|
17
|
+
*
|
|
18
|
+
* - `execute()` for GraphQL (routes mutations to `sdk.graphql.mutate` and
|
|
19
|
+
* everything else to `sdk.graphql.query`, with centralized error handling).
|
|
20
|
+
* Use with gql-tagged queries and generated operation types for type-safe
|
|
21
|
+
* calls.
|
|
22
|
+
* - `fetch()` for REST (proxies `sdk.fetch`, returning the raw `Response` so
|
|
23
|
+
* callers keep full control over response parsing / error shaping).
|
|
24
|
+
*
|
|
25
|
+
* Consumers inject this instead of calling `createDataSDK` directly, so the SDK
|
|
26
|
+
* bootstrap lives in exactly one place. Feature packages consume it via an
|
|
27
|
+
* `__inherit__data-client.service` stub (base wins at compose).
|
|
28
|
+
*/
|
|
29
|
+
@Injectable({ providedIn: 'root' })
|
|
30
|
+
export class DataClient {
|
|
31
|
+
/**
|
|
32
|
+
* Executes a GraphQL operation and returns its `data`, throwing on GraphQL
|
|
33
|
+
* errors or a null payload.
|
|
34
|
+
*/
|
|
35
|
+
async execute<TData, TVariables = Record<string, unknown>>(
|
|
36
|
+
operation: string,
|
|
37
|
+
variables?: TVariables,
|
|
38
|
+
): Promise<TData> {
|
|
39
|
+
const data = await createDataSDK();
|
|
40
|
+
const result = isMutation(operation)
|
|
41
|
+
? await data.graphql!.mutate<TData, TVariables>({
|
|
42
|
+
mutation: operation,
|
|
43
|
+
variables,
|
|
44
|
+
})
|
|
45
|
+
: await data.graphql!.query<TData, TVariables>({
|
|
46
|
+
query: operation,
|
|
47
|
+
variables,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
if (result.errors?.length) {
|
|
51
|
+
const msg = result.errors.map((e) => e.message).join('; ');
|
|
52
|
+
throw new Error(`GraphQL Error: ${msg}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (result.data == null) {
|
|
56
|
+
throw new Error('GraphQL response data is null');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return result.data;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Performs a REST request through the SDK's authenticated `fetch`, returning
|
|
64
|
+
* the raw `Response`. Callers parse the body / shape errors themselves (e.g.
|
|
65
|
+
* via `handleApiResponse`).
|
|
66
|
+
*
|
|
67
|
+
* @param input - The request URL (e.g. an Apex REST route).
|
|
68
|
+
* @param init - Standard `fetch` options (method, headers, body, …).
|
|
69
|
+
*/
|
|
70
|
+
async fetch(input: string, init?: RequestInit): Promise<Response> {
|
|
71
|
+
const sdk = await createDataSDK();
|
|
72
|
+
return sdk.fetch!(input, init);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -1,8 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2026, Salesforce, Inc.,
|
|
3
|
+
* All rights reserved.
|
|
4
|
+
* For full license text, see the LICENSE.txt file
|
|
5
|
+
*/
|
|
1
6
|
import { ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core';
|
|
2
7
|
import { provideRouter } from '@angular/router';
|
|
8
|
+
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
|
|
9
|
+
import { provideNativeDateAdapter } from '@angular/material/core';
|
|
3
10
|
|
|
4
11
|
import { routes } from './app.routes';
|
|
5
12
|
|
|
6
13
|
export const appConfig: ApplicationConfig = {
|
|
7
|
-
providers: [
|
|
14
|
+
providers: [
|
|
15
|
+
provideBrowserGlobalErrorListeners(),
|
|
16
|
+
provideRouter(routes),
|
|
17
|
+
provideAnimationsAsync(),
|
|
18
|
+
// Enables mat-datepicker (used by the object-search Date / DateRange
|
|
19
|
+
// filters) with the browser's native Date, avoiding a moment/luxon dep.
|
|
20
|
+
provideNativeDateAdapter(),
|
|
21
|
+
],
|
|
8
22
|
};
|
|
@@ -1,5 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2026, Salesforce, Inc.,
|
|
3
|
+
* All rights reserved.
|
|
4
|
+
* For full license text, see the LICENSE.txt file
|
|
5
|
+
*/
|
|
1
6
|
import { Routes } from '@angular/router';
|
|
2
|
-
import { AppLayoutComponent } from './layout/app-layout.component';
|
|
7
|
+
import { AppLayoutComponent } from './components/layout/app-layout/app-layout.component';
|
|
3
8
|
import { HomeComponent } from './pages/home/home.component';
|
|
4
9
|
import { NotFoundComponent } from './pages/not-found/not-found.component';
|
|
5
10
|
|
|
@@ -1,10 +1,15 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2026, Salesforce, Inc.,
|
|
3
|
+
* All rights reserved.
|
|
4
|
+
* For full license text, see the LICENSE.txt file
|
|
5
|
+
*/
|
|
6
|
+
import { ChangeDetectionStrategy, Component } from '@angular/core';
|
|
2
7
|
import { RouterOutlet } from '@angular/router';
|
|
3
8
|
|
|
4
9
|
@Component({
|
|
5
10
|
selector: 'app-root',
|
|
11
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
6
12
|
imports: [RouterOutlet],
|
|
7
13
|
templateUrl: './app.html',
|
|
8
|
-
styleUrl: './app.css',
|
|
9
14
|
})
|
|
10
15
|
export class App {}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
(click)="toggleMenu()"
|
|
7
7
|
class="p-2 rounded-md text-gray-700 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
8
8
|
aria-label="Toggle menu"
|
|
9
|
+
aria-controls="app-layout-nav-menu"
|
|
9
10
|
[attr.aria-expanded]="isOpen()"
|
|
10
11
|
>
|
|
11
12
|
<div class="w-6 h-6 flex flex-col justify-center space-y-1.5">
|
|
@@ -27,7 +28,7 @@
|
|
|
27
28
|
</button>
|
|
28
29
|
</div>
|
|
29
30
|
@if (isOpen()) {
|
|
30
|
-
<div class="pb-4">
|
|
31
|
+
<div id="app-layout-nav-menu" class="pb-4">
|
|
31
32
|
<ul class="flex flex-col space-y-2 list-none">
|
|
32
33
|
@for (item of navigationItems; track item.path) {
|
|
33
34
|
<li>
|
|
@@ -1,15 +1,21 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2026, Salesforce, Inc.,
|
|
3
|
+
* All rights reserved.
|
|
4
|
+
* For full license text, see the LICENSE.txt file
|
|
5
|
+
*/
|
|
6
|
+
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
|
|
2
7
|
import { RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router';
|
|
3
8
|
|
|
4
9
|
@Component({
|
|
5
10
|
selector: 'app-layout',
|
|
11
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
6
12
|
imports: [RouterLink, RouterLinkActive, RouterOutlet],
|
|
7
13
|
templateUrl: './app-layout.component.html',
|
|
8
14
|
})
|
|
9
15
|
export class AppLayoutComponent {
|
|
10
|
-
isOpen = signal(false);
|
|
16
|
+
readonly isOpen = signal(false);
|
|
11
17
|
|
|
12
|
-
navigationItems = [{ path: '/', label: 'Home' }];
|
|
18
|
+
readonly navigationItems = [{ path: '/', label: 'Home' }];
|
|
13
19
|
|
|
14
20
|
toggleMenu(): void {
|
|
15
21
|
this.isOpen.update((v) => !v);
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2026, Salesforce, Inc.,
|
|
3
|
+
* All rights reserved.
|
|
4
|
+
* For full license text, see the LICENSE.txt file
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/*
|
|
8
|
+
* Shared height scale for the form-field wrappers (app-input, app-select,
|
|
9
|
+
* app-date-picker): sm = 24px, default = 32px, lg = 40px.
|
|
10
|
+
*
|
|
11
|
+
* Material's density scale only reaches 36px at its -5 floor, so these compact
|
|
12
|
+
* heights can't come from `mat.form-field-density`. Instead we set the two M3
|
|
13
|
+
* tokens that drive the height directly:
|
|
14
|
+
* --mat-form-field-container-height -> `.mat-mdc-form-field-flex` min-height
|
|
15
|
+
* --mat-form-field-container-vertical-padding -> infix padding-top / -bottom
|
|
16
|
+
*
|
|
17
|
+
* These are CSS custom properties, so setting them on the wrapper's own
|
|
18
|
+
* `mat-form-field` (an in-view element that matches under emulated
|
|
19
|
+
* encapsulation) is enough — they INHERIT down to Material's internal flex /
|
|
20
|
+
* infix elements. No `::ng-deep` and no global classes needed. This mirrors how
|
|
21
|
+
* `app-button` sizes work (it sets `--mat-button-*-container-height` tokens).
|
|
22
|
+
*/
|
|
23
|
+
@mixin apply($height, $vertical-padding) {
|
|
24
|
+
--mat-form-field-container-height: #{$height};
|
|
25
|
+
--mat-form-field-container-vertical-padding: #{$vertical-padding};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/*
|
|
29
|
+
* Emit the three size classes. Consumers opt in via `size` on the wrapper,
|
|
30
|
+
* which renders `size-sm | size-default | size-lg` on the mat-form-field.
|
|
31
|
+
*/
|
|
32
|
+
@mixin sizes {
|
|
33
|
+
.size-sm {
|
|
34
|
+
@include apply(1.5rem, 0.25rem); // 24px, 4px
|
|
35
|
+
}
|
|
36
|
+
.size-default {
|
|
37
|
+
@include apply(2rem, 0.5rem); // 32px, 8px
|
|
38
|
+
}
|
|
39
|
+
.size-lg {
|
|
40
|
+
@include apply(2.5rem, 0.75rem); // 40px, 12px
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
@if (icon()) {
|
|
2
|
+
<mat-icon class="row-span-2 translate-y-0.5 !size-4 !text-base leading-4" aria-hidden="true">{{
|
|
3
|
+
icon()
|
|
4
|
+
}}</mat-icon>
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
@if (title()) {
|
|
8
|
+
<div data-slot="alert-title" class="col-start-2 font-medium">{{ title() }}</div>
|
|
9
|
+
}
|
|
10
|
+
@if (description()) {
|
|
11
|
+
<div
|
|
12
|
+
data-slot="alert-description"
|
|
13
|
+
class="col-start-2 text-sm"
|
|
14
|
+
[class.text-muted-foreground]="variant() === 'default'"
|
|
15
|
+
[class.text-destructive]="variant() === 'destructive'"
|
|
16
|
+
>
|
|
17
|
+
{{ description() }}
|
|
18
|
+
</div>
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
<!--
|
|
22
|
+
Default slot for callers that project body content (e.g. app-status-alert
|
|
23
|
+
composing a message with a link) instead of the `description` input. Sits in
|
|
24
|
+
the second grid column to align with the icon. Inert when only inputs are used.
|
|
25
|
+
-->
|
|
26
|
+
<div class="col-start-2 text-sm">
|
|
27
|
+
<ng-content />
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
<!-- Optional caller-owned trailing control, pinned top-right. -->
|
|
31
|
+
<ng-content select="app-alert-action" />
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2026, Salesforce, Inc.,
|
|
3
|
+
* All rights reserved.
|
|
4
|
+
* For full license text, see the LICENSE.txt file
|
|
5
|
+
*/
|
|
6
|
+
import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';
|
|
7
|
+
import { MatIconModule } from '@angular/material/icon';
|
|
8
|
+
|
|
9
|
+
/** Visual variant. */
|
|
10
|
+
export type AppAlertVariant = 'default' | 'destructive';
|
|
11
|
+
|
|
12
|
+
const BASE_CLASSES =
|
|
13
|
+
'relative w-full grid grid-cols-[auto_1fr] items-start gap-x-2 gap-y-0.5 rounded-lg border px-2.5 py-2 text-left text-sm has-[app-alert-action]:pr-18';
|
|
14
|
+
const VARIANT_CLASSES: Record<AppAlertVariant, string> = {
|
|
15
|
+
default: 'bg-card text-card-foreground',
|
|
16
|
+
destructive: 'bg-card text-destructive',
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/** Alert banner. Optional trailing control via the `app-alert-action` slot. */
|
|
20
|
+
@Component({
|
|
21
|
+
selector: 'app-alert',
|
|
22
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
23
|
+
imports: [MatIconModule],
|
|
24
|
+
templateUrl: './alert.component.html',
|
|
25
|
+
host: {
|
|
26
|
+
'data-slot': 'alert',
|
|
27
|
+
role: 'alert',
|
|
28
|
+
'[class]': 'hostClass()',
|
|
29
|
+
},
|
|
30
|
+
})
|
|
31
|
+
export class AlertComponent {
|
|
32
|
+
readonly variant = input<AppAlertVariant>('default');
|
|
33
|
+
readonly title = input<string>('');
|
|
34
|
+
readonly description = input<string>('');
|
|
35
|
+
/** Material icon ligature name, e.g. `info`, `error`, `warning`. */
|
|
36
|
+
readonly icon = input<string>('');
|
|
37
|
+
|
|
38
|
+
protected readonly hostClass = computed(
|
|
39
|
+
() => `${BASE_CLASSES} ${VARIANT_CLASSES[this.variant()]}`,
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Trailing action slot for `app-alert`, pinned to the top-right. */
|
|
44
|
+
@Component({
|
|
45
|
+
selector: 'app-alert-action',
|
|
46
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
47
|
+
template: '<ng-content />',
|
|
48
|
+
host: {
|
|
49
|
+
'data-slot': 'alert-action',
|
|
50
|
+
class: 'absolute top-2 right-2',
|
|
51
|
+
},
|
|
52
|
+
})
|
|
53
|
+
export class AlertActionComponent {}
|
package/lib/templates/uiBundles/angularbasic/src/app/components/ui/button/button.component.html
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<button
|
|
2
|
+
[matButton]="matAppearance()"
|
|
3
|
+
[class]="buttonClass()"
|
|
4
|
+
[class.destructive]="isDestructive()"
|
|
5
|
+
[type]="type()"
|
|
6
|
+
[disabled]="disabled()"
|
|
7
|
+
[attr.aria-label]="ariaLabel()"
|
|
8
|
+
[attr.aria-expanded]="ariaExpanded()"
|
|
9
|
+
(click)="clicked.emit($event)"
|
|
10
|
+
>
|
|
11
|
+
@if (icon()) {
|
|
12
|
+
<mat-icon aria-hidden="true">{{ icon() }}</mat-icon>
|
|
13
|
+
}
|
|
14
|
+
<ng-content />
|
|
15
|
+
@if (iconEnd()) {
|
|
16
|
+
<mat-icon aria-hidden="true" iconPositionEnd>{{ iconEnd() }}</mat-icon>
|
|
17
|
+
}
|
|
18
|
+
</button>
|
package/lib/templates/uiBundles/angularbasic/src/app/components/ui/button/button.component.scss
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2026, Salesforce, Inc.,
|
|
3
|
+
* All rights reserved.
|
|
4
|
+
* For full license text, see the LICENSE.txt file
|
|
5
|
+
*/
|
|
6
|
+
@use '@angular/material' as mat;
|
|
7
|
+
|
|
8
|
+
// Compact density themes — Material's canonical heights (36 / 32 / 28px).
|
|
9
|
+
$-lg: mat.define-theme(
|
|
10
|
+
(
|
|
11
|
+
density: (
|
|
12
|
+
scale: -1,
|
|
13
|
+
),
|
|
14
|
+
)
|
|
15
|
+
);
|
|
16
|
+
$-md: mat.define-theme(
|
|
17
|
+
(
|
|
18
|
+
density: (
|
|
19
|
+
scale: -2,
|
|
20
|
+
),
|
|
21
|
+
)
|
|
22
|
+
);
|
|
23
|
+
$-sm: mat.define-theme(
|
|
24
|
+
(
|
|
25
|
+
density: (
|
|
26
|
+
scale: -3,
|
|
27
|
+
),
|
|
28
|
+
)
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
@mixin -height($h) {
|
|
32
|
+
--mat-button-filled-container-height: #{$h};
|
|
33
|
+
--mat-button-outlined-container-height: #{$h};
|
|
34
|
+
--mat-button-text-container-height: #{$h};
|
|
35
|
+
--mat-button-tonal-container-height: #{$h};
|
|
36
|
+
--mat-button-protected-container-height: #{$h};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
@mixin -label-size($s) {
|
|
40
|
+
--mat-button-filled-label-text-size: #{$s};
|
|
41
|
+
--mat-button-outlined-label-text-size: #{$s};
|
|
42
|
+
--mat-button-text-label-text-size: #{$s};
|
|
43
|
+
--mat-button-tonal-label-text-size: #{$s};
|
|
44
|
+
--mat-button-protected-label-text-size: #{$s};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
:host {
|
|
48
|
+
display: inline-flex;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.mat-mdc-button-base {
|
|
52
|
+
width: 100%;
|
|
53
|
+
font-weight: 500;
|
|
54
|
+
--mat-button-filled-container-shape: var(--radius-md);
|
|
55
|
+
--mat-button-outlined-container-shape: var(--radius-md);
|
|
56
|
+
--mat-button-text-container-shape: var(--radius-md);
|
|
57
|
+
--mat-button-tonal-container-shape: var(--radius-md);
|
|
58
|
+
--mat-button-protected-container-shape: var(--radius-md);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Heights via Material's density scale; 24px (xs) sits below the button
|
|
62
|
+
// density floor (-3 = 28px), so it sets the height token directly.
|
|
63
|
+
.size-default,
|
|
64
|
+
.size-icon {
|
|
65
|
+
@include mat.button-density($-md);
|
|
66
|
+
}
|
|
67
|
+
.size-sm,
|
|
68
|
+
.size-icon-sm {
|
|
69
|
+
@include mat.button-density($-sm);
|
|
70
|
+
}
|
|
71
|
+
.size-lg,
|
|
72
|
+
.size-icon-lg {
|
|
73
|
+
@include mat.button-density($-lg);
|
|
74
|
+
}
|
|
75
|
+
.size-xs,
|
|
76
|
+
.size-icon-xs {
|
|
77
|
+
@include -height(1.5rem);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Font-size isn't a density concern, so it stays a label token.
|
|
81
|
+
.size-default,
|
|
82
|
+
.size-lg {
|
|
83
|
+
@include -label-size(0.875rem);
|
|
84
|
+
}
|
|
85
|
+
.size-sm {
|
|
86
|
+
@include -label-size(0.8rem);
|
|
87
|
+
}
|
|
88
|
+
.size-xs {
|
|
89
|
+
@include -label-size(0.75rem);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Icon-only: square, no min-width or horizontal padding. Material's leading-icon
|
|
93
|
+
// margins assume a text label, so zero them to keep the icon centred.
|
|
94
|
+
.size-icon,
|
|
95
|
+
.size-icon-xs,
|
|
96
|
+
.size-icon-sm,
|
|
97
|
+
.size-icon-lg {
|
|
98
|
+
width: auto;
|
|
99
|
+
min-width: 0;
|
|
100
|
+
padding: 0;
|
|
101
|
+
aspect-ratio: 1;
|
|
102
|
+
|
|
103
|
+
.mat-icon {
|
|
104
|
+
margin: 0;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Destructive: subtle red tint + red text, matching React's bg-destructive/10.
|
|
109
|
+
.destructive {
|
|
110
|
+
--mat-button-filled-container-color: color-mix(in oklch, var(--destructive) 10%, transparent);
|
|
111
|
+
--mat-button-filled-label-text-color: var(--destructive);
|
|
112
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2026, Salesforce, Inc.,
|
|
3
|
+
* All rights reserved.
|
|
4
|
+
* For full license text, see the LICENSE.txt file
|
|
5
|
+
*/
|
|
6
|
+
import { ChangeDetectionStrategy, Component, computed, input, output } from '@angular/core';
|
|
7
|
+
import { MatButtonModule, type MatButtonAppearance } from '@angular/material/button';
|
|
8
|
+
import { MatIconModule } from '@angular/material/icon';
|
|
9
|
+
|
|
10
|
+
/** Material's appearances plus `destructive`, which Material doesn't provide. */
|
|
11
|
+
export type AppButtonAppearance = MatButtonAppearance | 'destructive';
|
|
12
|
+
|
|
13
|
+
/** Button size variants. */
|
|
14
|
+
export type AppButtonSize =
|
|
15
|
+
| 'default'
|
|
16
|
+
| 'xs'
|
|
17
|
+
| 'sm'
|
|
18
|
+
| 'lg'
|
|
19
|
+
| 'icon'
|
|
20
|
+
| 'icon-xs'
|
|
21
|
+
| 'icon-sm'
|
|
22
|
+
| 'icon-lg';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Thin wrapper over `matButton`. Adds a `destructive` appearance and a `size`
|
|
26
|
+
* scale Material lacks; inputs drive the inner button (a host class/aria attr
|
|
27
|
+
* would land on `<app-button>`, not the real `<button>`).
|
|
28
|
+
*/
|
|
29
|
+
@Component({
|
|
30
|
+
selector: 'app-button',
|
|
31
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
32
|
+
imports: [MatButtonModule, MatIconModule],
|
|
33
|
+
templateUrl: './button.component.html',
|
|
34
|
+
styleUrl: './button.component.scss',
|
|
35
|
+
})
|
|
36
|
+
export class ButtonComponent {
|
|
37
|
+
readonly appearance = input<AppButtonAppearance>('filled');
|
|
38
|
+
readonly size = input<AppButtonSize>('default');
|
|
39
|
+
readonly type = input<'button' | 'submit' | 'reset'>('button');
|
|
40
|
+
readonly disabled = input<boolean>(false);
|
|
41
|
+
readonly ariaLabel = input<string>();
|
|
42
|
+
readonly ariaExpanded = input<boolean>();
|
|
43
|
+
/** Extra classes for the inner button — `class` on the host forwards here. */
|
|
44
|
+
readonly classes = input<string>('', { alias: 'class' });
|
|
45
|
+
/** Leading / trailing Material icon ligature names, e.g. `search`. */
|
|
46
|
+
readonly icon = input<string>('');
|
|
47
|
+
readonly iconEnd = input<string>('');
|
|
48
|
+
readonly clicked = output<MouseEvent>();
|
|
49
|
+
|
|
50
|
+
/** `destructive` renders as a filled button carrying the destructive class. */
|
|
51
|
+
protected readonly matAppearance = computed<MatButtonAppearance>(() =>
|
|
52
|
+
this.appearance() === 'destructive' ? 'filled' : (this.appearance() as MatButtonAppearance),
|
|
53
|
+
);
|
|
54
|
+
protected readonly isDestructive = computed(() => this.appearance() === 'destructive');
|
|
55
|
+
/** Size class merged with any caller-supplied classes, applied to the button. */
|
|
56
|
+
protected readonly buttonClass = computed(() => `size-${this.size()} ${this.classes()}`.trim());
|
|
57
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2026, Salesforce, Inc.,
|
|
3
|
+
* All rights reserved.
|
|
4
|
+
* For full license text, see the LICENSE.txt file
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// `size="sm"` — compact padding. Material hardcodes the card's 16px padding
|
|
8
|
+
// (there is no padding design token to override), so the compact size tightens
|
|
9
|
+
// the real Material padding classes. `::ng-deep` is required because the slot
|
|
10
|
+
// content (`mat-card-header/content/actions`) is projected — it lives in the
|
|
11
|
+
// consumer's view, not this wrapper's — so a plain scoped selector can't reach
|
|
12
|
+
// it. The rules are anchored under `.app-card-sm`, a class only this component
|
|
13
|
+
// applies, so the deep selector stays contained to compact cards.
|
|
14
|
+
.app-card-sm {
|
|
15
|
+
::ng-deep .mat-mdc-card-header {
|
|
16
|
+
padding: 0.75rem 0.75rem 0;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
::ng-deep .mat-mdc-card-content {
|
|
20
|
+
padding: 0 0.75rem;
|
|
21
|
+
|
|
22
|
+
&:first-child {
|
|
23
|
+
padding-top: 0.75rem;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
&:last-child {
|
|
27
|
+
padding-bottom: 0.75rem;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
::ng-deep .mat-mdc-card-actions {
|
|
32
|
+
padding: 0.75rem;
|
|
33
|
+
}
|
|
34
|
+
}
|