@salesforce/ui-bundle-template-app-angular-template-b2e 11.30.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.
Files changed (161) hide show
  1. package/LICENSE.txt +82 -0
  2. package/dist/.forceignore +15 -0
  3. package/dist/.husky/pre-commit +4 -0
  4. package/dist/.prettierignore +11 -0
  5. package/dist/.prettierrc +17 -0
  6. package/dist/CHANGELOG.md +3966 -0
  7. package/dist/README.md +77 -0
  8. package/dist/config/project-scratch-def.json +13 -0
  9. package/dist/eslint.config.js +7 -0
  10. package/dist/force-app/main/default/applications/angularinternalapp.app-meta.xml +17 -0
  11. package/dist/force-app/main/default/permissionsets/angularinternalapp_Access.permissionset-meta.xml +13 -0
  12. package/dist/force-app/main/default/uiBundles/angularinternalapp/.forceignore +15 -0
  13. package/dist/force-app/main/default/uiBundles/angularinternalapp/.postcssrc.json +5 -0
  14. package/dist/force-app/main/default/uiBundles/angularinternalapp/.prettierignore +8 -0
  15. package/dist/force-app/main/default/uiBundles/angularinternalapp/.prettierrc +12 -0
  16. package/dist/force-app/main/default/uiBundles/angularinternalapp/CHANGELOG.md +4 -0
  17. package/dist/force-app/main/default/uiBundles/angularinternalapp/README.md +85 -0
  18. package/dist/force-app/main/default/uiBundles/angularinternalapp/angular.json +98 -0
  19. package/dist/force-app/main/default/uiBundles/angularinternalapp/angularinternalapp.uibundle-meta.xml +8 -0
  20. package/dist/force-app/main/default/uiBundles/angularinternalapp/e2e/app.spec.ts +15 -0
  21. package/dist/force-app/main/default/uiBundles/angularinternalapp/esbuild/api-version.mjs +17 -0
  22. package/dist/force-app/main/default/uiBundles/angularinternalapp/eslint.config.js +60 -0
  23. package/dist/force-app/main/default/uiBundles/angularinternalapp/middleware/html.mjs +3 -0
  24. package/dist/force-app/main/default/uiBundles/angularinternalapp/middleware/proxy.mjs +12 -0
  25. package/dist/force-app/main/default/uiBundles/angularinternalapp/package.json +57 -0
  26. package/dist/force-app/main/default/uiBundles/angularinternalapp/playwright.config.ts +25 -0
  27. package/dist/force-app/main/default/uiBundles/angularinternalapp/public/favicon.ico +0 -0
  28. package/dist/force-app/main/default/uiBundles/angularinternalapp/scripts/rewrite-e2e-assets.mjs +30 -0
  29. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/api/account/account-search.service.ts +196 -0
  30. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/api/account/distinctAccountIndustries.ts +26 -0
  31. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/api/account/distinctAccountTypes.ts +26 -0
  32. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/api/account/getAccountDetail.ts +128 -0
  33. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/api/account/searchAccounts.ts +58 -0
  34. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/api/data-client.service.spec.ts +184 -0
  35. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/api/data-client.service.ts +74 -0
  36. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/app.config.ts +22 -0
  37. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/app.html +1 -0
  38. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/app.routes.ts +31 -0
  39. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/app.spec.ts +18 -0
  40. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/app.ts +15 -0
  41. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/components/layout/app-layout/app-layout.component.html +52 -0
  42. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/components/layout/app-layout/app-layout.component.spec.ts +90 -0
  43. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/components/layout/app-layout/app-layout.component.ts +67 -0
  44. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/components/ui/_field-size.scss +42 -0
  45. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/components/ui/alert/alert.component.html +31 -0
  46. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/components/ui/alert/alert.component.ts +53 -0
  47. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/components/ui/button/button.component.html +18 -0
  48. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/components/ui/button/button.component.scss +112 -0
  49. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/components/ui/button/button.component.ts +57 -0
  50. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/components/ui/card/card.component.scss +34 -0
  51. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/components/ui/card/card.component.ts +110 -0
  52. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/components/ui/collapsible/collapsible.component.html +10 -0
  53. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/components/ui/collapsible/collapsible.component.ts +26 -0
  54. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/components/ui/date-picker/date-picker.component.html +25 -0
  55. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/components/ui/date-picker/date-picker.component.scss +36 -0
  56. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/components/ui/date-picker/date-picker.component.ts +41 -0
  57. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/components/ui/date-range-picker/date-range-picker.component.html +34 -0
  58. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/components/ui/date-range-picker/date-range-picker.component.scss +33 -0
  59. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/components/ui/date-range-picker/date-range-picker.component.ts +66 -0
  60. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/components/ui/dialog/dialog.component.html +37 -0
  61. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/components/ui/dialog/dialog.component.ts +55 -0
  62. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/components/ui/field/field.component.html +21 -0
  63. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/components/ui/field/field.component.ts +36 -0
  64. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/components/ui/field-size.ts +8 -0
  65. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/components/ui/input/input.component.html +19 -0
  66. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/components/ui/input/input.component.scss +35 -0
  67. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/components/ui/input/input.component.ts +36 -0
  68. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/components/ui/label/label.component.html +3 -0
  69. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/components/ui/label/label.component.ts +17 -0
  70. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/components/ui/paginator/paginator.component.html +10 -0
  71. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/components/ui/paginator/paginator.component.ts +37 -0
  72. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/components/ui/popover/popover.component.html +41 -0
  73. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/components/ui/popover/popover.component.ts +40 -0
  74. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/components/ui/select/select.component.html +24 -0
  75. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/components/ui/select/select.component.scss +35 -0
  76. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/components/ui/select/select.component.ts +72 -0
  77. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/components/ui/separator/separator.component.html +1 -0
  78. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/components/ui/separator/separator.component.scss +23 -0
  79. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/components/ui/separator/separator.component.ts +56 -0
  80. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/components/ui/skeleton/skeleton.component.html +1 -0
  81. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/components/ui/skeleton/skeleton.component.scss +28 -0
  82. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/components/ui/skeleton/skeleton.component.ts +24 -0
  83. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/components/ui/spinner/spinner.component.ts +35 -0
  84. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/features/agentforce/agentforce-conversation-client/agentforce-conversation-client.component.html +3 -0
  85. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/features/agentforce/agentforce-conversation-client/agentforce-conversation-client.component.ts +119 -0
  86. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/features/agentforce/agentforce-embed.service.ts +177 -0
  87. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/features/object-search/components/active-filters/active-filters.component.html +23 -0
  88. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/features/object-search/components/active-filters/active-filters.component.ts +78 -0
  89. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/features/object-search/components/filters/date-filter/date-filter.component.html +18 -0
  90. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/features/object-search/components/filters/date-filter/date-filter.component.ts +137 -0
  91. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/features/object-search/components/filters/date-range-filter/date-range-filter.component.html +11 -0
  92. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/features/object-search/components/filters/date-range-filter/date-range-filter.component.ts +69 -0
  93. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/features/object-search/components/filters/filter-field-wrapper/filter-field-wrapper.component.html +11 -0
  94. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/features/object-search/components/filters/filter-field-wrapper/filter-field-wrapper.component.ts +36 -0
  95. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/features/object-search/components/filters/multi-select-filter/multi-select-filter.component.html +10 -0
  96. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/features/object-search/components/filters/multi-select-filter/multi-select-filter.component.ts +58 -0
  97. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/features/object-search/components/filters/numeric-range-filter/numeric-range-filter.component.html +22 -0
  98. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/features/object-search/components/filters/numeric-range-filter/numeric-range-filter.component.ts +146 -0
  99. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/features/object-search/components/filters/search-filter/search-filter.component.html +8 -0
  100. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/features/object-search/components/filters/search-filter/search-filter.component.ts +82 -0
  101. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/features/object-search/components/filters/select-filter/select-filter.component.html +12 -0
  102. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/features/object-search/components/filters/select-filter/select-filter.component.ts +65 -0
  103. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/features/object-search/components/filters/text-filter/text-filter.component.html +12 -0
  104. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/features/object-search/components/filters/text-filter/text-filter.component.ts +90 -0
  105. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/features/object-search/components/object-breadcrumb/object-breadcrumb.component.html +55 -0
  106. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/features/object-search/components/object-breadcrumb/object-breadcrumb.component.ts +40 -0
  107. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/features/object-search/components/pagination-controls/pagination-controls.component.html +8 -0
  108. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/features/object-search/components/pagination-controls/pagination-controls.component.ts +61 -0
  109. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/features/object-search/components/search-bar/search-bar.component.html +59 -0
  110. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/features/object-search/components/search-bar/search-bar.component.ts +108 -0
  111. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/features/object-search/components/sort-control/sort-control.component.html +50 -0
  112. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/features/object-search/components/sort-control/sort-control.component.ts +72 -0
  113. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/features/object-search/object-search-state.service.ts +232 -0
  114. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/features/object-search/utils/debounce.ts +31 -0
  115. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/features/object-search/utils/field-utils.ts +54 -0
  116. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/features/object-search/utils/filter-utils.ts +354 -0
  117. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/features/object-search/utils/index.ts +12 -0
  118. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/features/object-search/utils/sort-utils.ts +46 -0
  119. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/pages/account-detail/account-detail-page.component.html +174 -0
  120. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/pages/account-detail/account-detail-page.component.scss +7 -0
  121. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/pages/account-detail/account-detail-page.component.ts +199 -0
  122. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/pages/account-search/account-search-page.component.html +111 -0
  123. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/pages/account-search/account-search-page.component.ts +217 -0
  124. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/pages/home/home-page.component.html +18 -0
  125. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/pages/home/home-page.component.ts +47 -0
  126. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/pages/home/home.component.html +6 -0
  127. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/pages/home/home.component.spec.ts +23 -0
  128. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/pages/home/home.component.ts +13 -0
  129. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/pages/not-found/not-found.component.html +12 -0
  130. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/pages/not-found/not-found.component.spec.ts +33 -0
  131. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/pages/not-found/not-found.component.ts +15 -0
  132. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/types/conversation.ts +55 -0
  133. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/types/globals.d.ts +13 -0
  134. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/utils/async-data.ts +99 -0
  135. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/index.html +14 -0
  136. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/index.ts +15 -0
  137. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/main.ts +5 -0
  138. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/styles.scss +170 -0
  139. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/theme.scss +39 -0
  140. package/dist/force-app/main/default/uiBundles/angularinternalapp/src/types/sf-globals.d.ts +9 -0
  141. package/dist/force-app/main/default/uiBundles/angularinternalapp/tsconfig.app.json +11 -0
  142. package/dist/force-app/main/default/uiBundles/angularinternalapp/tsconfig.json +33 -0
  143. package/dist/force-app/main/default/uiBundles/angularinternalapp/tsconfig.spec.json +10 -0
  144. package/dist/force-app/main/default/uiBundles/angularinternalapp/ui-bundle.json +7 -0
  145. package/dist/jest.config.js +6 -0
  146. package/dist/package-lock.json +10066 -0
  147. package/dist/package.json +46 -0
  148. package/dist/scripts/apex/hello.apex +10 -0
  149. package/dist/scripts/gitignore-templates.json +4 -0
  150. package/dist/scripts/graphql-search.sh +191 -0
  151. package/dist/scripts/org-setup-config-schema.mjs +103 -0
  152. package/dist/scripts/org-setup-dev.mjs +87 -0
  153. package/dist/scripts/org-setup-utils.mjs +369 -0
  154. package/dist/scripts/org-setup-xml.mjs +182 -0
  155. package/dist/scripts/org-setup.config.json +9 -0
  156. package/dist/scripts/org-setup.mjs +1702 -0
  157. package/dist/scripts/sf-project-setup.mjs +103 -0
  158. package/dist/scripts/soql/account.soql +6 -0
  159. package/dist/scripts/validate-org-setup-config.mjs +38 -0
  160. package/dist/sfdx-project.json +12 -0
  161. package/package.json +40 -0
@@ -0,0 +1,199 @@
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, inject, signal } from "@angular/core";
7
+ import { ActivatedRoute, Router } from "@angular/router";
8
+ import { toSignal } from "@angular/core/rxjs-interop";
9
+ import { map } from "rxjs/operators";
10
+ // Base-app UI wrappers, inherited (not recreated) by this feature. Post-compose
11
+ // they land at uiBundles/<target>/src/app/components/ui/..., three levels up from this
12
+ // page folder (app/features/account-search/ -> app/components/ui/).
13
+ import { AlertComponent } from "../../components/ui/alert/alert.component";
14
+ import { ButtonComponent } from "../../components/ui/button/button.component";
15
+ import {
16
+ CardComponent,
17
+ CardContentComponent,
18
+ } from "../../components/ui/card/card.component";
19
+ import { CollapsibleComponent } from "../../components/ui/collapsible/collapsible.component";
20
+ import { SeparatorComponent } from "../../components/ui/separator/separator.component";
21
+ import { SkeletonComponent } from "../../components/ui/skeleton/skeleton.component";
22
+ // Object-search feature building blocks (Waves 1-2), two levels up.
23
+ import { ObjectBreadcrumbComponent } from "../../features/object-search/components/object-breadcrumb/object-breadcrumb.component";
24
+ import { createAsyncData } from "../../utils/async-data";
25
+ import {
26
+ fieldValue,
27
+ getAddressFieldLines,
28
+ formatDateTimeField,
29
+ } from "../../features/object-search/utils/field-utils";
30
+ import { AccountSearchService } from "../../api/account/account-search.service";
31
+
32
+ /** `Intl` options for the created / last-modified timestamps (matches React). */
33
+ const DATE_TIME_OPTIONS: Intl.DateTimeFormatOptions = {
34
+ dateStyle: "medium",
35
+ timeStyle: "short",
36
+ };
37
+
38
+ /** A `{ label, value }` pair rendered in a field grid. */
39
+ interface Field {
40
+ label: string;
41
+ value: string | null;
42
+ /** `true` renders the value as a `tel:` link (Phone / Fax). */
43
+ telephone?: boolean;
44
+ }
45
+
46
+ /**
47
+ * Account detail page. The Angular analogue of the React
48
+ * `AccountObjectDetailPage`: a breadcrumb plus a card of collapsible sections
49
+ * (top fields, Additional / Address / System information), with loading,
50
+ * error, and not-found states.
51
+ *
52
+ * The record id comes from the `:recordId` route param (reactively, so a
53
+ * client-side nav between two detail routes refetches). Data flows through
54
+ * {@link createAsyncData} for latest-wins fetching.
55
+ */
56
+ @Component({
57
+ selector: "app-account-detail-page",
58
+ changeDetection: ChangeDetectionStrategy.OnPush,
59
+ imports: [
60
+ AlertComponent,
61
+ ButtonComponent,
62
+ CardComponent,
63
+ CardContentComponent,
64
+ CollapsibleComponent,
65
+ SeparatorComponent,
66
+ SkeletonComponent,
67
+ ObjectBreadcrumbComponent,
68
+ ],
69
+ templateUrl: "./account-detail-page.component.html",
70
+ styleUrl: "./account-detail-page.component.scss",
71
+ })
72
+ export class AccountDetailPageComponent {
73
+ private readonly service = inject(AccountSearchService);
74
+ private readonly route = inject(ActivatedRoute);
75
+ private readonly router = inject(Router);
76
+
77
+ /** Reactive `:recordId` param — drives (and re-drives) the fetch. */
78
+ private readonly recordId = toSignal(
79
+ this.route.paramMap.pipe(map((p) => p.get("recordId") ?? "")),
80
+ { initialValue: this.route.snapshot.paramMap.get("recordId") ?? "" },
81
+ );
82
+
83
+ private readonly result = createAsyncData(this.recordId, (id) =>
84
+ this.service.getAccountDetail(id),
85
+ );
86
+
87
+ protected readonly account = this.result.data;
88
+ protected readonly loading = this.result.loading;
89
+ protected readonly error = this.result.error;
90
+ /** True once a resolved fetch returned no record (distinct from loading). */
91
+ protected readonly notFound = computed(() => !this.loading() && !this.error() && !this.account());
92
+
93
+ /** Per-section collapsible open state (all default open, matching React). */
94
+ protected readonly additionalOpen = signal<boolean>(true);
95
+ protected readonly addressOpen = signal<boolean>(true);
96
+ protected readonly systemOpen = signal<boolean>(true);
97
+
98
+ /** Placeholder rows for skeleton field grids. */
99
+ protected readonly skeletonRows = [0, 1, 2];
100
+
101
+ // -- Breadcrumb leaf label -------------------------------------------------
102
+ protected readonly recordName = computed<string | undefined>(() => {
103
+ const a = this.account();
104
+ if (a) return fieldValue(a.Name) ?? "";
105
+ if (this.error()) return "Error";
106
+ if (this.loading()) return undefined;
107
+ return "Not Found";
108
+ });
109
+
110
+ // -- Header ----------------------------------------------------------------
111
+ protected readonly title = computed(() => fieldValue(this.account()?.Name ?? null) ?? "");
112
+
113
+ // -- Top field rows --------------------------------------------------------
114
+ protected readonly topRows = computed<Field[][]>(() => {
115
+ const a = this.account();
116
+ if (!a) return [];
117
+ return [
118
+ [
119
+ { label: "Account Owner", value: fieldValue(a.Owner?.Name) },
120
+ { label: "Phone", value: fieldValue(a.Phone), telephone: true },
121
+ ],
122
+ [
123
+ { label: "Account Name", value: fieldValue(a.Name) },
124
+ { label: "Fax", value: fieldValue(a.Fax), telephone: true },
125
+ ],
126
+ [
127
+ { label: "Parent Account", value: fieldValue(a.Parent?.Name) },
128
+ { label: "Website", value: fieldValue(a.Website) },
129
+ ],
130
+ ];
131
+ });
132
+
133
+ // -- Additional Information ------------------------------------------------
134
+ protected readonly additionalRows = computed<Field[][]>(() => {
135
+ const a = this.account();
136
+ if (!a) return [];
137
+ return [
138
+ [
139
+ { label: "Type", value: fieldValue(a.Type) },
140
+ { label: "Employees", value: fieldValue(a.NumberOfEmployees) },
141
+ ],
142
+ [
143
+ { label: "Industry", value: fieldValue(a.Industry) },
144
+ { label: "Annual Revenue", value: fieldValue(a.AnnualRevenue) },
145
+ ],
146
+ ];
147
+ });
148
+ protected readonly description = computed(() => fieldValue(this.account()?.Description ?? null));
149
+
150
+ // -- Address Information ---------------------------------------------------
151
+ protected readonly billingAddress = computed<string[] | null>(() => {
152
+ const a = this.account();
153
+ if (!a) return null;
154
+ return getAddressFieldLines({
155
+ street: fieldValue(a.BillingStreet),
156
+ city: fieldValue(a.BillingCity),
157
+ state: fieldValue(a.BillingState),
158
+ postalCode: fieldValue(a.BillingPostalCode),
159
+ country: fieldValue(a.BillingCountry),
160
+ });
161
+ });
162
+ protected readonly shippingAddress = computed<string[] | null>(() => {
163
+ const a = this.account();
164
+ if (!a) return null;
165
+ return getAddressFieldLines({
166
+ street: fieldValue(a.ShippingStreet),
167
+ city: fieldValue(a.ShippingCity),
168
+ state: fieldValue(a.ShippingState),
169
+ postalCode: fieldValue(a.ShippingPostalCode),
170
+ country: fieldValue(a.ShippingCountry),
171
+ });
172
+ });
173
+
174
+ // -- System Information ----------------------------------------------------
175
+ protected readonly createdBy = computed(() =>
176
+ this.joinNameAndDate(this.account()?.CreatedBy?.Name, this.account()?.CreatedDate),
177
+ );
178
+ protected readonly lastModifiedBy = computed(() =>
179
+ this.joinNameAndDate(this.account()?.LastModifiedBy?.Name, this.account()?.LastModifiedDate),
180
+ );
181
+
182
+ private joinNameAndDate(
183
+ name: { value?: unknown; displayValue?: string | null } | null | undefined,
184
+ date: { value?: unknown; displayValue?: string | null } | null | undefined,
185
+ ): string | null {
186
+ const formatted = formatDateTimeField(fieldValue(date ?? null), undefined, DATE_TIME_OPTIONS);
187
+ return [fieldValue(name ?? null), formatted].filter(Boolean).join(" ") || null;
188
+ }
189
+
190
+ /** Navigate back to the previous view (mirrors the React `navigate(-1)`). */
191
+ protected goBack(): void {
192
+ void this.router.navigate(["/accounts"]);
193
+ }
194
+
195
+ /** Full page reload retry, matching the React "Retry" affordance. */
196
+ protected reload(): void {
197
+ window.location.reload();
198
+ }
199
+ }
@@ -0,0 +1,111 @@
1
+ <div class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
2
+ <app-object-breadcrumb listPath="/accounts" listLabel="Accounts" />
3
+
4
+ <h1 class="text-2xl font-bold mb-4">Search Accounts</h1>
5
+
6
+ <div class="flex flex-col lg:flex-row gap-6">
7
+ <!-- Sidebar — Filter Panel -->
8
+ <aside class="w-full lg:w-80 shrink-0">
9
+ <app-collapsible title="Filters" [(open)]="filtersOpen">
10
+ <div class="space-y-1">
11
+ <app-search-filter [config]="searchConfig" />
12
+ <app-text-filter [config]="nameConfig" />
13
+ <app-select-filter [config]="industryConfig()" />
14
+ <app-multi-select-filter [config]="typeConfig()" />
15
+ <app-numeric-range-filter [config]="revenueConfig" />
16
+ <app-date-filter [config]="createdConfig" filterType="datetime" />
17
+ <app-date-range-filter [config]="lastModifiedConfig" filterType="datetimerange" />
18
+ </div>
19
+ </app-collapsible>
20
+ </aside>
21
+
22
+ <!-- Main area — Sort + Results -->
23
+ <div class="flex-1 min-w-0">
24
+ <div class="flex flex-wrap items-center gap-2 mb-4">
25
+ <app-sort-control [sortFields]="sortConfigs" />
26
+ <app-active-filters />
27
+ </div>
28
+
29
+ <div class="min-h-132" [attr.aria-busy]="loading()">
30
+ @if (loading()) {
31
+ <app-skeleton class="h-5 w-30 mb-3" />
32
+ <div class="divide-y">
33
+ @for (row of skeletonRows(); track row) {
34
+ <div class="flex items-center justify-between py-3">
35
+ <div class="space-y-2">
36
+ <app-skeleton class="h-5 w-40" />
37
+ <app-skeleton class="h-4 w-28" />
38
+ </div>
39
+ <div class="space-y-2 flex flex-col items-end">
40
+ <app-skeleton class="h-4 w-24" />
41
+ <app-skeleton class="h-4 w-20" />
42
+ </div>
43
+ </div>
44
+ }
45
+ </div>
46
+ } @else if (error()) {
47
+ <p class="text-sm text-muted-foreground mb-3">0 accounts found</p>
48
+ <app-alert
49
+ variant="destructive"
50
+ icon="error"
51
+ title="Failed to load accounts"
52
+ description="Something went wrong while loading accounts. Please try again later."
53
+ />
54
+ } @else if (nodes().length > 0) {
55
+ <p class="text-sm text-muted-foreground mb-3">{{ resultCountLabel() }}</p>
56
+ <ul class="divide-y">
57
+ @for (node of nodes(); track node.Id) {
58
+ <li>
59
+ <a
60
+ [routerLink]="['/accounts', node.Id]"
61
+ class="flex items-center justify-between py-3 px-3 -mx-3 rounded-md transition-colors hover:bg-accent"
62
+ >
63
+ <div>
64
+ <span class="font-medium">{{ name(node) }}</span>
65
+ <p class="text-sm text-muted-foreground">{{ secondaryLine(node) }}</p>
66
+ </div>
67
+ <div class="text-right text-sm">
68
+ <p>{{ phone(node) }}</p>
69
+ <p class="text-muted-foreground">{{ ownerName(node) }}</p>
70
+ </div>
71
+ </a>
72
+ </li>
73
+ }
74
+ </ul>
75
+ } @else {
76
+ <div class="flex flex-col items-center justify-center py-16 text-center">
77
+ <svg
78
+ xmlns="http://www.w3.org/2000/svg"
79
+ class="size-12 text-muted-foreground mb-4"
80
+ viewBox="0 0 24 24"
81
+ fill="none"
82
+ stroke="currentColor"
83
+ stroke-width="2"
84
+ stroke-linecap="round"
85
+ stroke-linejoin="round"
86
+ aria-hidden="true"
87
+ >
88
+ <path d="m13.5 8.5-5 5" />
89
+ <path d="m8.5 8.5 5 5" />
90
+ <circle cx="11" cy="11" r="8" />
91
+ <path d="m21 21-4.3-4.3" />
92
+ </svg>
93
+ <h2 class="text-lg font-semibold mb-1">No accounts found</h2>
94
+ <p class="text-sm text-muted-foreground">
95
+ Try adjusting your filters or search criteria.
96
+ </p>
97
+ </div>
98
+ }
99
+ </div>
100
+
101
+ <!-- Pagination — always visible, disabled while loading or on error -->
102
+ <app-pagination-controls
103
+ [totalCount]="totalCount()"
104
+ [hasNextPage]="hasNextPage()"
105
+ [endCursor]="endCursor()"
106
+ [pageSizeOptions]="pageSizeOptions"
107
+ [disabled]="controlsDisabled()"
108
+ />
109
+ </div>
110
+ </div>
111
+ </div>
@@ -0,0 +1,217 @@
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, inject, signal } from "@angular/core";
7
+ import { RouterLink } from "@angular/router";
8
+ // Base-app UI wrappers, inherited (not recreated) by this feature. Post-compose
9
+ // they land at uiBundles/<target>/src/app/components/ui/..., three levels up from this
10
+ // page folder (app/features/account-search/ -> app/components/ui/).
11
+ import { AlertComponent } from "../../components/ui/alert/alert.component";
12
+ import { CollapsibleComponent } from "../../components/ui/collapsible/collapsible.component";
13
+ import { SkeletonComponent } from "../../components/ui/skeleton/skeleton.component";
14
+ // Object-search feature building blocks (Waves 1-2), two levels up.
15
+ import { ObjectBreadcrumbComponent } from "../../features/object-search/components/object-breadcrumb/object-breadcrumb.component";
16
+ import { SortControlComponent } from "../../features/object-search/components/sort-control/sort-control.component";
17
+ import { ActiveFiltersComponent } from "../../features/object-search/components/active-filters/active-filters.component";
18
+ import { PaginationControlsComponent } from "../../features/object-search/components/pagination-controls/pagination-controls.component";
19
+ import { SearchFilterComponent } from "../../features/object-search/components/filters/search-filter/search-filter.component";
20
+ import { TextFilterComponent } from "../../features/object-search/components/filters/text-filter/text-filter.component";
21
+ import { SelectFilterComponent } from "../../features/object-search/components/filters/select-filter/select-filter.component";
22
+ import { MultiSelectFilterComponent } from "../../features/object-search/components/filters/multi-select-filter/multi-select-filter.component";
23
+ import { NumericRangeFilterComponent } from "../../features/object-search/components/filters/numeric-range-filter/numeric-range-filter.component";
24
+ import { DateFilterComponent } from "../../features/object-search/components/filters/date-filter/date-filter.component";
25
+ import { DateRangeFilterComponent } from "../../features/object-search/components/filters/date-range-filter/date-range-filter.component";
26
+ import {
27
+ ObjectSearchStateService,
28
+ type PaginationConfig,
29
+ } from "../../features/object-search/object-search-state.service";
30
+ import { createAsyncData } from "../../utils/async-data";
31
+ import { fieldValue } from "../../features/object-search/utils/field-utils";
32
+ import type { FilterFieldConfig } from "../../features/object-search/utils/filter-utils";
33
+ import type { SortFieldConfig } from "../../features/object-search/utils/sort-utils";
34
+ import { AccountSearchService, type AccountNode } from "../../api/account/account-search.service";
35
+
36
+ /** Cursor-pagination config for the account search (mirrors the React reference). */
37
+ const PAGINATION_CONFIG: PaginationConfig = {
38
+ defaultPageSize: 7,
39
+ validPageSizes: [7, 14, 28, 42],
40
+ };
41
+
42
+ /**
43
+ * Filter field configs — the single source of truth for URL (de)serialization
44
+ * and GraphQL `where` building. The picklist option lists for Industry / Type
45
+ * are filled in at runtime (see the component's computed configs) from the
46
+ * distinct-value aggregate queries.
47
+ */
48
+ const FILTER_CONFIGS: FilterFieldConfig[] = [
49
+ {
50
+ field: "search",
51
+ label: "Search",
52
+ type: "search",
53
+ searchFields: ["Name", "Phone", "Industry"],
54
+ placeholder: "Search by name, phone, or industry...",
55
+ },
56
+ { field: "Name", label: "Account Name", type: "text", placeholder: "Search by name..." },
57
+ { field: "Industry", label: "Industry", type: "picklist" },
58
+ { field: "Type", label: "Type", type: "multipicklist" },
59
+ { field: "AnnualRevenue", label: "Annual Revenue", type: "numeric" },
60
+ { field: "CreatedDate", label: "Created Date", type: "datetime", disableFuture: true },
61
+ {
62
+ field: "LastModifiedDate",
63
+ label: "Last Modified Date",
64
+ type: "datetimerange",
65
+ disableFuture: true,
66
+ },
67
+ ];
68
+
69
+ /** Sortable fields exposed by the sort control (mirrors the React reference). */
70
+ const ACCOUNT_SORT_CONFIGS: SortFieldConfig[] = [
71
+ { field: "Name", label: "Name" },
72
+ { field: "AnnualRevenue", label: "Annual Revenue" },
73
+ { field: "Industry", label: "Industry" },
74
+ { field: "CreatedDate", label: "Created Date" },
75
+ ];
76
+
77
+ /**
78
+ * Account search / list page. The Angular analogue of the React `AccountSearch`
79
+ * example page: a sidebar filter panel (Card + Collapsible) plus a main results
80
+ * area with sort control, active-filter chips, a results list, and cursor
81
+ * pagination.
82
+ *
83
+ * State lives in a page-scoped {@link ObjectSearchStateService} (provided here,
84
+ * NOT in root) so each search page owns its own filters / sort / cursor stack.
85
+ * The filter components projected into the sidebar inject that same instance.
86
+ *
87
+ * Data flows through {@link createAsyncData}: whenever the derived query args
88
+ * (where / orderBy / pageSize / afterCursor) change, the search re-runs with
89
+ * latest-wins semantics.
90
+ */
91
+ @Component({
92
+ selector: "app-account-search-page",
93
+ changeDetection: ChangeDetectionStrategy.OnPush,
94
+ providers: [ObjectSearchStateService],
95
+ imports: [
96
+ RouterLink,
97
+ AlertComponent,
98
+ CollapsibleComponent,
99
+ SkeletonComponent,
100
+ ObjectBreadcrumbComponent,
101
+ SortControlComponent,
102
+ ActiveFiltersComponent,
103
+ PaginationControlsComponent,
104
+ SearchFilterComponent,
105
+ TextFilterComponent,
106
+ SelectFilterComponent,
107
+ MultiSelectFilterComponent,
108
+ NumericRangeFilterComponent,
109
+ DateFilterComponent,
110
+ DateRangeFilterComponent,
111
+ ],
112
+ templateUrl: "./account-search-page.component.html",
113
+ })
114
+ export class AccountSearchPageComponent {
115
+ private readonly service = inject(AccountSearchService);
116
+ private readonly state = inject(ObjectSearchStateService);
117
+
118
+ /** Sortable field configs surfaced to the sort control. */
119
+ protected readonly sortConfigs = ACCOUNT_SORT_CONFIGS;
120
+ /** Allowed page sizes forwarded to the pagination controls. */
121
+ protected readonly pageSizeOptions = PAGINATION_CONFIG.validPageSizes;
122
+
123
+ /** Collapsible open state for the sidebar filter panel. */
124
+ protected readonly filtersOpen = signal<boolean>(true);
125
+
126
+ /** Derived, API-ready `where` / `orderBy` signals from the shared state. */
127
+ private readonly query = this.state.init<unknown, unknown>(FILTER_CONFIGS, PAGINATION_CONFIG);
128
+
129
+ /** Live page-size / cursor signals from the shared state. */
130
+ protected readonly pageSize = this.state.pageSize;
131
+ protected readonly hasPreviousPage = this.state.hasPreviousPage;
132
+
133
+ // -- Distinct-value option loads (Industry / Type filters) -----------------
134
+ private readonly industries = createAsyncData(signal(0), () =>
135
+ this.service.fetchDistinctIndustries(),
136
+ );
137
+ private readonly types = createAsyncData(signal(0), () => this.service.fetchDistinctTypes());
138
+
139
+ // -- Per-filter configs (options merged in for the picklist filters) -------
140
+ protected readonly searchConfig = FILTER_CONFIGS[0];
141
+ protected readonly nameConfig = FILTER_CONFIGS[1];
142
+ protected readonly revenueConfig = FILTER_CONFIGS[4];
143
+ protected readonly createdConfig = FILTER_CONFIGS[5];
144
+ protected readonly lastModifiedConfig = FILTER_CONFIGS[6];
145
+ protected readonly industryConfig = computed<FilterFieldConfig>(() => ({
146
+ ...FILTER_CONFIGS[2],
147
+ options: this.industries.data() ?? [],
148
+ }));
149
+ protected readonly typeConfig = computed<FilterFieldConfig>(() => ({
150
+ ...FILTER_CONFIGS[3],
151
+ options: this.types.data() ?? [],
152
+ }));
153
+
154
+ // -- Search results --------------------------------------------------------
155
+ private readonly searchArgs = computed(() => ({
156
+ where: this.query.where(),
157
+ orderBy: this.query.orderBy(),
158
+ first: this.state.pageSize(),
159
+ after: this.state.afterCursor(),
160
+ }));
161
+ private readonly result = createAsyncData(this.searchArgs, (args) =>
162
+ this.service.searchAccounts(args),
163
+ );
164
+
165
+ protected readonly loading = this.result.loading;
166
+ protected readonly error = this.result.error;
167
+
168
+ private readonly pageInfo = computed(() => this.result.data()?.pageInfo);
169
+ protected readonly totalCount = computed(() => this.result.data()?.totalCount);
170
+ protected readonly hasNextPage = computed(() => this.pageInfo()?.hasNextPage ?? false);
171
+ protected readonly endCursor = computed(() => this.pageInfo()?.endCursor ?? undefined);
172
+
173
+ /** Non-null account nodes from the current page's edges. */
174
+ protected readonly nodes = computed<AccountNode[]>(() => {
175
+ const edges = this.result.data()?.edges ?? [];
176
+ const out: AccountNode[] = [];
177
+ for (const edge of edges) {
178
+ if (edge?.node) out.push(edge.node);
179
+ }
180
+ return out;
181
+ });
182
+
183
+ /** Pagination controls (and the results list) are inert while fetching/errored. */
184
+ protected readonly controlsDisabled = computed(() => this.loading() || !!this.error());
185
+
186
+ /** Placeholder rows for the loading skeleton — one per requested page size. */
187
+ protected readonly skeletonRows = computed(() =>
188
+ Array.from({ length: this.pageSize() }, (_, i) => i),
189
+ );
190
+
191
+ /** Result-count copy above the list, matching the React reference wording. */
192
+ protected readonly resultCountLabel = computed(() => {
193
+ const total = this.totalCount();
194
+ const shown = this.nodes().length;
195
+ if (total != null && (this.hasNextPage() || this.hasPreviousPage())) {
196
+ return `${total} account${total !== 1 ? "s" : ""} found`;
197
+ }
198
+ return `Showing ${shown} account${shown !== 1 ? "s" : ""}`;
199
+ });
200
+
201
+ /** Display helpers for the results list (mirrors `fieldValue` in the template). */
202
+ protected name(node: AccountNode): string {
203
+ return fieldValue(node.Name) ?? "—";
204
+ }
205
+
206
+ protected secondaryLine(node: AccountNode): string {
207
+ return [fieldValue(node.Industry), fieldValue(node.Type)].filter(Boolean).join(" · ") || "—";
208
+ }
209
+
210
+ protected phone(node: AccountNode): string {
211
+ return fieldValue(node.Phone) ?? "";
212
+ }
213
+
214
+ protected ownerName(node: AccountNode): string {
215
+ return fieldValue(node.Owner?.Name) ?? "";
216
+ }
217
+ }
@@ -0,0 +1,18 @@
1
+ <div class="max-w-2xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
2
+ <div class="flex items-center gap-6 mb-6">
3
+ <h1 class="text-2xl font-bold">Account Search</h1>
4
+ <app-button appearance="outlined" size="sm" (clicked)="browseAll()"
5
+ >Browse All Accounts</app-button
6
+ >
7
+ </div>
8
+ <form class="flex gap-2 items-center" (submit)="onSubmit($event)">
9
+ <app-input
10
+ class="flex-1"
11
+ type="text"
12
+ placeholder="Search by name, phone, or industry..."
13
+ label="Search"
14
+ [(value)]="term"
15
+ />
16
+ <app-button type="submit">Search</app-button>
17
+ </form>
18
+ </div>
@@ -0,0 +1,47 @@
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, inject, signal } from "@angular/core";
7
+ import { Router } from "@angular/router";
8
+ // Base-app UI wrappers, inherited (not recreated) by this feature. Post-compose
9
+ // they land at uiBundles/<target>/src/app/components/ui/{button,input}/..., three levels
10
+ // up from this page folder (app/features/account-search/ -> app/components/ui/).
11
+ import { ButtonComponent } from "../../components/ui/button/button.component";
12
+ import { InputComponent } from "../../components/ui/input/input.component";
13
+
14
+ /**
15
+ * Account-search landing page. Mirrors the React `Home` page: a single search
16
+ * box plus a "Browse All Accounts" shortcut. Submitting navigates to the
17
+ * `accounts` list, carrying the typed term as the `q` query param so the search
18
+ * page's {@link ObjectSearchStateService} seeds the multi-field search filter.
19
+ *
20
+ * This is intentionally NOT the state-service-driven `app-search-bar` — Home
21
+ * has no search state of its own; it just routes into the search page.
22
+ */
23
+ @Component({
24
+ selector: "app-home-page",
25
+ changeDetection: ChangeDetectionStrategy.OnPush,
26
+ imports: [ButtonComponent, InputComponent],
27
+ templateUrl: "./home-page.component.html",
28
+ })
29
+ export class HomePageComponent {
30
+ private readonly router = inject(Router);
31
+
32
+ /** Bound to the search input; forwarded as `?q=` on submit. */
33
+ protected readonly term = signal<string>("");
34
+
35
+ /** Navigate to the search page, seeding the search term when present. */
36
+ protected onSubmit(event: Event): void {
37
+ event.preventDefault();
38
+ this.browseAll(this.term() || undefined);
39
+ }
40
+
41
+ /** Go to the accounts list, optionally with a seeded search term. */
42
+ protected browseAll(q?: string): void {
43
+ void this.router.navigate(["/accounts"], {
44
+ queryParams: q ? { q } : {},
45
+ });
46
+ }
47
+ }
@@ -0,0 +1,6 @@
1
+ <main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
2
+ <div class="text-center">
3
+ <h1 class="text-4xl font-bold text-gray-900 mb-4">Home</h1>
4
+ <p class="text-lg text-gray-600 mb-8">Welcome to your Angular application.</p>
5
+ </div>
6
+ </main>
@@ -0,0 +1,23 @@
1
+ import { TestBed } from '@angular/core/testing';
2
+ import { HomeComponent } from './home.component';
3
+
4
+ describe('HomeComponent', () => {
5
+ beforeEach(async () => {
6
+ await TestBed.configureTestingModule({
7
+ imports: [HomeComponent],
8
+ }).compileComponents();
9
+ });
10
+
11
+ it('should create the component', () => {
12
+ const fixture = TestBed.createComponent(HomeComponent);
13
+ const component = fixture.componentInstance;
14
+ expect(component).toBeTruthy();
15
+ });
16
+
17
+ it('should render main landmark', async () => {
18
+ const fixture = TestBed.createComponent(HomeComponent);
19
+ await fixture.whenStable();
20
+ const compiled = fixture.nativeElement as HTMLElement;
21
+ expect(compiled.querySelector('main')).toBeTruthy();
22
+ });
23
+ });
@@ -0,0 +1,13 @@
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';
7
+
8
+ @Component({
9
+ selector: 'app-home',
10
+ changeDetection: ChangeDetectionStrategy.OnPush,
11
+ templateUrl: './home.component.html',
12
+ })
13
+ export class HomeComponent {}
@@ -0,0 +1,12 @@
1
+ <main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
2
+ <div class="text-center">
3
+ <h1 class="text-4xl font-bold text-gray-900 mb-4">404</h1>
4
+ <p class="text-lg text-gray-600 mb-8">Page not found</p>
5
+ <a
6
+ routerLink="/"
7
+ class="inline-block px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors"
8
+ >
9
+ Go to Home
10
+ </a>
11
+ </div>
12
+ </main>