├── .all-contributorsrc
├── .commitlintrc.json
├── .editorconfig
├── .eslintignore
├── .eslintrc.json
├── .gitattributes
├── .github
├── actions
│ └── install-npm-deps
│ │ └── action.yml
├── copilot-instructions.md
├── dependabot.yml
├── funding.yml
└── workflows
│ ├── main.yml
│ ├── release-please.yml
│ └── release.yml
├── .gitignore
├── .husky
├── commit-msg
└── pre-commit
├── .lintstagedrc.json
├── .mcprc.json
├── .npmrc
├── .nvmrc
├── .prettierignore
├── .prettierrc.json
├── .vscode
├── extensions.json
├── launch.json
├── settings.json
└── tasks.json
├── AGENTS.md
├── CHANGELOG.md
├── CODEOWNERS
├── CODE_OF_CONDUCT.md
├── LICENSE
├── README.md
├── angular.json
├── jest-global-mocks.ts
├── jest.config.ts
├── mcp.json
├── package-lock.json
├── package.json
├── projects
├── demo-e2e
│ ├── artifacts
│ │ └── .gitignore
│ ├── cypress.config.ts
│ ├── src
│ │ ├── fixtures
│ │ │ └── .gitkeep
│ │ ├── index.d.ts
│ │ ├── support
│ │ │ ├── commands.ts
│ │ │ ├── e2e.ts
│ │ │ └── utils.ts
│ │ └── tests
│ │ │ └── demo.cy.ts
│ └── tsconfig.json
├── demo
│ ├── .eslintrc.json
│ ├── jest-setup.ts
│ ├── jest.config.ts
│ ├── src
│ │ ├── app
│ │ │ ├── __snapshots__
│ │ │ │ └── app.component.spec.ts.snap
│ │ │ ├── app.component.html
│ │ │ ├── app.component.scss
│ │ │ ├── app.component.spec.ts
│ │ │ ├── app.component.ts
│ │ │ ├── app.config.ts
│ │ │ └── example
│ │ │ │ ├── __snapshots__
│ │ │ │ └── example.component.spec.ts.snap
│ │ │ │ ├── example.component.html
│ │ │ │ ├── example.component.scss
│ │ │ │ ├── example.component.spec.ts
│ │ │ │ ├── example.component.ts
│ │ │ │ └── section-options.pipe.ts
│ │ ├── assets
│ │ │ └── .gitkeep
│ │ ├── favicon.ico
│ │ ├── index.html
│ │ ├── main.ts
│ │ └── styles.scss
│ ├── tsconfig.app.json
│ └── tsconfig.spec.json
├── example-e2e
│ ├── artifacts
│ │ └── .gitignore
│ ├── cypress.config.ts
│ ├── src
│ │ ├── fixtures
│ │ │ └── .gitkeep
│ │ ├── index.d.ts
│ │ ├── support
│ │ │ ├── commands.ts
│ │ │ └── e2e.ts
│ │ └── tests
│ │ │ └── page-highlighting.cy.ts
│ └── tsconfig.json
├── example
│ ├── .eslintrc.json
│ ├── jest-setup.ts
│ ├── jest.config.ts
│ ├── src
│ │ ├── app
│ │ │ ├── app.component.html
│ │ │ ├── app.component.scss
│ │ │ ├── app.component.ts
│ │ │ ├── app.config.ts
│ │ │ ├── app.routes.ts
│ │ │ ├── page-highlighting
│ │ │ │ ├── __snapshots__
│ │ │ │ │ └── page-highlighting.component.spec.ts.snap
│ │ │ │ ├── page-highlighting.component.html
│ │ │ │ ├── page-highlighting.component.scss
│ │ │ │ ├── page-highlighting.component.spec.ts
│ │ │ │ └── page-highlighting.component.ts
│ │ │ ├── page-infinite-scroll
│ │ │ │ ├── page-infinite-scroll.component.html
│ │ │ │ ├── page-infinite-scroll.component.scss
│ │ │ │ └── page-infinite-scroll.component.ts
│ │ │ └── page-lazy-images
│ │ │ │ ├── lazy-image-skeleton
│ │ │ │ ├── lazy-image-skeleton.component.html
│ │ │ │ ├── lazy-image-skeleton.component.scss
│ │ │ │ └── lazy-image-skeleton.component.ts
│ │ │ │ ├── lazy-image.directive.ts
│ │ │ │ ├── page-lazy-images.component.html
│ │ │ │ ├── page-lazy-images.component.scss
│ │ │ │ └── page-lazy-images.component.ts
│ │ ├── assets
│ │ │ └── .gitkeep
│ │ ├── favicon.ico
│ │ ├── index.html
│ │ ├── main.ts
│ │ └── styles.scss
│ ├── tsconfig.app.json
│ └── tsconfig.spec.json
└── ng-in-viewport
│ ├── .eslintrc.json
│ ├── jest-setup.ts
│ ├── jest.config.ts
│ ├── ng-package.json
│ ├── package.json
│ ├── src
│ ├── lib
│ │ ├── directives
│ │ │ ├── destroyable.directive.spec.ts
│ │ │ ├── destroyable.directive.ts
│ │ │ ├── in-viewport.directive.spec.ts
│ │ │ ├── in-viewport.directive.ts
│ │ │ └── index.ts
│ │ ├── enums
│ │ │ ├── in-viewport-direction.ts
│ │ │ └── index.ts
│ │ ├── exceptions
│ │ │ ├── index.ts
│ │ │ ├── invalid-direction.exception.spec.ts
│ │ │ ├── invalid-direction.exception.ts
│ │ │ ├── invalid-root-margin.exception.spec.ts
│ │ │ ├── invalid-root-margin.exception.ts
│ │ │ ├── invalid-root-node.exception.spec.ts
│ │ │ ├── invalid-root-node.exception.ts
│ │ │ ├── invalid-threshold.exception.spec.ts
│ │ │ └── invalid-threshold.exception.ts
│ │ ├── in-viewport.module.ts
│ │ ├── services
│ │ │ ├── in-viewport.service.spec.ts
│ │ │ ├── in-viewport.service.ts
│ │ │ └── index.ts
│ │ ├── utils
│ │ │ ├── index.ts
│ │ │ ├── is-object.spec.ts
│ │ │ ├── is-object.ts
│ │ │ ├── observer-cache-item.spec.ts
│ │ │ ├── observer-cache-item.ts
│ │ │ ├── observer-cache.spec.ts
│ │ │ ├── observer-cache.ts
│ │ │ ├── stringify.spec.ts
│ │ │ ├── stringify.ts
│ │ │ ├── to-base64.spec.ts
│ │ │ └── to-base64.ts
│ │ └── values
│ │ │ ├── check-fn.spec.ts
│ │ │ ├── check-fn.ts
│ │ │ ├── config.spec.ts
│ │ │ ├── config.ts
│ │ │ ├── direction.spec.ts
│ │ │ ├── direction.ts
│ │ │ ├── index.ts
│ │ │ ├── partial.spec.ts
│ │ │ ├── partial.ts
│ │ │ ├── root-margin.spec.ts
│ │ │ ├── root-margin.ts
│ │ │ ├── root-node.spec.ts
│ │ │ ├── root-node.ts
│ │ │ ├── threshold.spec.ts
│ │ │ └── threshold.ts
│ └── public-api.ts
│ ├── tsconfig.lib.json
│ ├── tsconfig.lib.prod.json
│ └── tsconfig.spec.json
└── tsconfig.json
/.all-contributorsrc:
--------------------------------------------------------------------------------
1 | {
2 | "projectName": "ng-in-viewport",
3 | "projectOwner": "k3nsei",
4 | "repoType": "github",
5 | "repoHost": "https://github.com",
6 | "files": [
7 | "README.md"
8 | ],
9 | "imageSize": 100,
10 | "commit": false,
11 | "commitConvention": "angular",
12 | "contributors": [
13 | {
14 | "login": "k3nsei",
15 | "name": "Piotr Stępniewski",
16 | "avatar_url": "https://avatars2.githubusercontent.com/u/190422?v=4",
17 | "profile": "https://github.com/k3nsei",
18 | "contributions": [
19 | "code",
20 | "doc",
21 | "review",
22 | "test"
23 | ]
24 | },
25 | {
26 | "login": "Bengejd",
27 | "name": "Jordan Benge",
28 | "avatar_url": "https://avatars3.githubusercontent.com/u/11723093?v=4",
29 | "profile": "https://github.com/Bengejd",
30 | "contributions": [
31 | "blog"
32 | ]
33 | },
34 | {
35 | "login": "numerized",
36 | "name": "Kévin Perrée",
37 | "avatar_url": "https://avatars1.githubusercontent.com/u/166829?v=4",
38 | "profile": "https://github.com/numerized",
39 | "contributions": [
40 | "bug"
41 | ]
42 | },
43 | {
44 | "login": "OzoTek",
45 | "name": "Alexandre Couret",
46 | "avatar_url": "https://avatars3.githubusercontent.com/u/6436053?v=4",
47 | "profile": "https://github.com/OzoTek",
48 | "contributions": [
49 | "bug"
50 | ]
51 | },
52 | {
53 | "login": "anwar-elmawardy",
54 | "name": "anwar-elmawardy",
55 | "avatar_url": "https://avatars0.githubusercontent.com/u/23740710?v=4",
56 | "profile": "https://github.com/anwar-elmawardy",
57 | "contributions": [
58 | "bug"
59 | ]
60 | },
61 | {
62 | "login": "jwillebrands",
63 | "name": "Jan-Willem Willebrands",
64 | "avatar_url": "https://avatars0.githubusercontent.com/u/8925?v=4",
65 | "profile": "https://github.com/jwillebrands",
66 | "contributions": [
67 | "bug"
68 | ]
69 | },
70 | {
71 | "login": "CSchulz",
72 | "name": "CSchulz ",
73 | "avatar_url": "https://avatars2.githubusercontent.com/u/1520593?v=4",
74 | "profile": "https://github.com/CSchulz",
75 | "contributions": [
76 | "bug"
77 | ]
78 | },
79 | {
80 | "login": "Silvest89",
81 | "name": "Johnnie Ho",
82 | "avatar_url": "https://avatars2.githubusercontent.com/u/2388338?v=4",
83 | "profile": "https://github.com/Silvest89",
84 | "contributions": [
85 | "bug"
86 | ]
87 | },
88 | {
89 | "login": "pasevin",
90 | "name": "Aleksandr Pasevin",
91 | "avatar_url": "https://avatars2.githubusercontent.com/u/1058469?v=4",
92 | "profile": "https://github.com/pasevin",
93 | "contributions": [
94 | "code",
95 | "bug"
96 | ]
97 | },
98 | {
99 | "login": "wkurniawan07",
100 | "name": "Wilson Kurniawan",
101 | "avatar_url": "https://avatars2.githubusercontent.com/u/7261051?v=4",
102 | "profile": "https://github.com/wkurniawan07",
103 | "contributions": [
104 | "bug"
105 | ]
106 | },
107 | {
108 | "login": "basters",
109 | "name": "Eugene",
110 | "avatar_url": "https://avatars0.githubusercontent.com/u/17099950?v=4",
111 | "profile": "https://github.com/basters",
112 | "contributions": [
113 | "code"
114 | ]
115 | },
116 | {
117 | "login": "samizarraa",
118 | "name": "Sami Zarraa",
119 | "avatar_url": "https://avatars3.githubusercontent.com/u/20872538?v=4",
120 | "profile": "https://github.com/samizarraa",
121 | "contributions": [
122 | "bug"
123 | ]
124 | },
125 | {
126 | "login": "Jonnyprof",
127 | "name": "JordiJS",
128 | "avatar_url": "https://avatars.githubusercontent.com/u/9952131?v=4",
129 | "profile": "https://github.com/Jonnyprof",
130 | "contributions": [
131 | "bug"
132 | ]
133 | },
134 | {
135 | "login": "mpschaeuble",
136 | "name": "mpschaeuble",
137 | "avatar_url": "https://avatars.githubusercontent.com/u/18322360?v=4",
138 | "profile": "https://github.com/mpschaeuble",
139 | "contributions": [
140 | "bug"
141 | ]
142 | },
143 | {
144 | "login": "karptonite",
145 | "name": "Daniel Karp",
146 | "avatar_url": "https://avatars.githubusercontent.com/u/132278?v=4",
147 | "profile": "https://github.com/karptonite",
148 | "contributions": [
149 | "bug"
150 | ]
151 | },
152 | {
153 | "login": "tinesoft",
154 | "name": "Tine Kondo",
155 | "avatar_url": "https://avatars.githubusercontent.com/u/4053092?v=4",
156 | "profile": "https://github.com/tinesoft",
157 | "contributions": [
158 | "ideas", "review"
159 | ]
160 | },
161 | {
162 | "login": "BojanKogoj",
163 | "name": "Bojan Kogoj",
164 | "avatar_url": "https://avatars.githubusercontent.com/u/634075?v=4",
165 | "profile": "https://github.com/BojanKogoj",
166 | "contributions": [
167 | "doc"
168 | ]
169 | }
170 | ],
171 | "contributorsPerLine": 7,
172 | "linkToUsage": false,
173 | "skipCi": true,
174 | "commitType": "docs"
175 | }
176 |
--------------------------------------------------------------------------------
/.commitlintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["@commitlint/config-conventional"]
3 | }
4 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # Editor configuration, see https://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 | end_of_line = lf
7 | indent_style = space
8 | indent_size = 2
9 | insert_final_newline = true
10 | trim_trailing_whitespace = true
11 |
12 | [*.ts]
13 | quote_type = single
14 |
15 | [*.md]
16 | max_line_length = off
17 | trim_trailing_whitespace = false
18 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | /dist
2 | /out-tsc
3 | /tmp
4 | /coverage
5 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "ignorePatterns": ["projects/**/*"],
4 | "settings": {
5 | "import/resolver": {
6 | "typescript": true,
7 | "node": true
8 | }
9 | },
10 | "overrides": [
11 | {
12 | "files": ["*.ts"],
13 | "extends": [
14 | "eslint:recommended",
15 | "plugin:@typescript-eslint/recommended",
16 | "plugin:@angular-eslint/recommended",
17 | "plugin:@angular-eslint/template/process-inline-templates",
18 | "plugin:import/recommended",
19 | "plugin:import/typescript",
20 | "plugin:prettier/recommended"
21 | ],
22 | "rules": {
23 | "sort-imports": [
24 | "error",
25 | {
26 | "ignoreDeclarationSort": true,
27 | "allowSeparatedGroups": true
28 | }
29 | ],
30 | "import/first": "error",
31 | "import/order": [
32 | "error",
33 | {
34 | "alphabetize": { "order": "asc", "caseInsensitive": true },
35 | "newlines-between": "always",
36 | "groups": [
37 | ["builtin", "external"],
38 | "internal",
39 | "parent",
40 | ["sibling", "index"],
41 | "object",
42 | "type"
43 | ]
44 | }
45 | ],
46 | "@typescript-eslint/no-unused-vars": [
47 | "error",
48 | {
49 | "argsIgnorePattern": "^_",
50 | "caughtErrors": "all",
51 | "caughtErrorsIgnorePattern": "^(_|ignore)",
52 | "destructuredArrayIgnorePattern": "^_",
53 | "ignoreRestSiblings": true
54 | }
55 | ],
56 | "@typescript-eslint/explicit-member-accessibility": [
57 | "error",
58 | {
59 | "accessibility": "explicit",
60 | "overrides": {
61 | "constructors": "no-public"
62 | }
63 | }
64 | ]
65 | }
66 | },
67 | {
68 | "files": ["*.html"],
69 | "extends": [
70 | "plugin:@angular-eslint/template/recommended",
71 | "plugin:prettier/recommended"
72 | ],
73 | "rules": {}
74 | }
75 | ]
76 | }
77 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | .gitattributes !filter !diff
2 |
3 | # Source code
4 | *.coffee text
5 | *.css text
6 | *.htm text diff=html
7 | *.html text diff=html
8 | *.inc text
9 | *.js text
10 | *.json text
11 | *.jsx text
12 | *.less text
13 | *.ls text
14 | *.map text -diff
15 | *.od text
16 | *.onlydata text
17 | *.phtml text diff=php
18 | *.php text diff=php
19 | *.pl text
20 | *.py text diff=python
21 | *.rb text diff=ruby
22 | *.sass text
23 | *.scm text
24 | *.scss text diff=css
25 | *.sql text
26 | *.styl text
27 | *.tag text
28 | *.ts text
29 | *.tsx text
30 | *.xml text
31 | *.xhtml text diff=html
32 |
33 | # Templates
34 | *.dot text
35 | *.ejs text
36 | *.haml text
37 | *.handlebars text
38 | *.hbs text
39 | *.hbt text
40 | *.jade text
41 | *.latte text
42 | *.mustache text
43 | *.njk text
44 | *.phtml text
45 | *.tmpl text
46 | *.tpl text
47 | *.twig text
48 | *.vue text
49 |
50 | # Configs
51 | *.cnf text
52 | *.conf text
53 | *.config text
54 | *.ini text
55 | *.lock text -diff
56 | *.toml text
57 | *.xml text
58 | *.yml text
59 | *.yaml text
60 | .editorconfig text
61 | .env text
62 | .gitconfig text
63 | .htaccess text
64 | browserslist text
65 | Makefile text
66 | makefile text
67 | package-lock.json text -diff
68 |
69 | # Documentation
70 | *.ipynb text
71 | *.markdown text
72 | *.md text
73 | *.mdwn text
74 | *.mdown text
75 | *.mdx text
76 | *.mkd text
77 | *.mkdn text
78 | *.mdtxt text
79 | *.mdtext text
80 | AUTHORS text
81 | CHANGELOG text
82 | CHANGES text
83 | CONTRIBUTING text
84 | COPYING text
85 | copyright text
86 | *COPYRIGHT* text
87 | INSTALL text
88 | license text
89 | LICENSE text
90 | NEWS text
91 | readme text
92 | *README* text
93 | TODO text
94 |
95 | # Scripts
96 | *.bash text eol=lf
97 | *.bat text eol=crlf
98 | *.cmd ext eol=crlf
99 | *.fish text eol=lf
100 | *.ps1 text eol=crlf
101 | *.sh text eol=lf
102 |
103 | # Docker
104 | Dockerfile text
105 |
106 | # Heroku
107 | Procfile text
108 |
109 | # Text
110 | *.textile text eol=lf
111 | *.txt text eol=lf
112 |
113 | # Data
114 | *.csv text eol=lf
115 |
116 | # Fonts
117 | *.eot binary
118 | *.otf binary
119 | *.ttf binary
120 | *.woff binary
121 | *.woff2 binary
122 |
123 | # Images
124 | *.ai binary
125 | *.apng binary
126 | *.avif binary
127 | *.bmp binary
128 | *.cur binary
129 | *.eps binary
130 | *.gif binary
131 | *.gifv binary
132 | *.ico binary
133 | *.jfif binary
134 | *.jng binary
135 | *.jp2 binary
136 | *.jpg binary
137 | *.jpeg binary
138 | *.jpx binary
139 | *.jxr binary
140 | *.pdf binary
141 | *.pjp binary
142 | *.pjpeg binary
143 | *.png binary
144 | *.psb binary
145 | *.psd binary
146 | *.svg text
147 | *.svgz binary
148 | *.tif binary
149 | *.tiff binary
150 | *.wbmp binary
151 | *.webp binary
152 |
153 | # Audio
154 | *.kar binary
155 | *.m4a binary
156 | *.mid binary
157 | *.midi binary
158 | *.mp3 binary
159 | *.ogg binary
160 | *.ra binary
161 |
162 | # Videos
163 | *.3gp binary
164 | *.3gpp binary
165 | *.as binary
166 | *.asf binary
167 | *.asx binary
168 | *.fla binary
169 | *.flv binary
170 | *.m4v binary
171 | *.mkv binary
172 | *.mng binary
173 | *.mov binary
174 | *.mp4 binary
175 | *.mpg binary
176 | *.mpeg binary
177 | *.ogv binary
178 | *.swc binary
179 | *.swf binary
180 | *.webm binary
181 |
182 | # Archives
183 | *.7z binary
184 | *.gz binary
185 | *.jar binary
186 | *.rar binary
187 | *.tar binary
188 | *.tgz binary
189 | *.zip binary
190 |
191 | # Executables
192 | *.bin binary
193 | *.deb binary
194 | *.dll binary
195 | *.elf binary
196 | *.exe binary
197 | *.msi binary
198 | *.pyc binary
199 | *.so binary
200 |
201 | # Ignore files
202 | *.*ignore text
203 |
204 | # RC files
205 | *.*rc text
206 |
207 | # Auto detect text files and perform LF normalization
208 | * text=auto
209 |
--------------------------------------------------------------------------------
/.github/actions/install-npm-deps/action.yml:
--------------------------------------------------------------------------------
1 | name: Setup Node.js and install NPM dependencies
2 | description: Install Node.js and NPM, then install NPM dependencies
3 |
4 | inputs:
5 | node-version:
6 | description: 'Which version of Node.js to install'
7 | required: false
8 | default: 'lts/*'
9 | registry-url:
10 | description: 'Which NPM registry url to use'
11 | required: false
12 | default: 'https://registry.npmjs.org'
13 |
14 | runs:
15 | using: composite
16 | steps:
17 | - name: Setup Node.js and NPM
18 | uses: actions/setup-node@v3
19 | with:
20 | node-version: ${{ inputs.node-version }}
21 | registry-url: ${{ inputs.registry-url }}
22 | cache: 'npm'
23 | cache-dependency-path: '**/package-lock.json'
24 |
25 | - name: Install NPM dependencies
26 | shell: bash
27 | run: npm ci
28 |
--------------------------------------------------------------------------------
/.github/copilot-instructions.md:
--------------------------------------------------------------------------------
1 | # GitHub Copilot Instructions for ng-in-viewport
2 |
3 | **Angular Development Guidelines**
4 | _Ignore current project patterns - use only latest Angular v20+ standards and best practices_
5 |
6 | ## Project Overview
7 |
8 | This is an Angular library for viewport detection using modern Angular v20+ patterns. The repository contains:
9 |
10 | - **Core library**: `projects/ng-in-viewport` - Signal-based viewport detection with Intersection Observer
11 | - **Demo application**: `projects/demo` - Documentation and interactive examples
12 | - **Example application**: `projects/example` - Real-world usage scenarios and performance testing
13 | - **Test suites**: Comprehensive unit and E2E testing with modern Angular testing patterns
14 |
15 | ## Environment Setup
16 |
17 | ### Node.js Requirements
18 |
19 | - **Required**: Node.js >=22.0.0 (LTS)
20 | - **Required**: npm >=10.0.0
21 | - **Package Manager**: Prefer `npm` with exact versions for consistency
22 |
23 | ### Dependency Installation
24 |
25 | For restricted environments, use network workarounds:
26 |
27 | ```bash
28 | CYPRESS_INSTALL_BINARY=0 npm ci
29 | ```
30 |
31 | **Execution time**: ~15 seconds
32 | **Timeout**: Use 60+ seconds minimum, NEVER CANCEL
33 |
34 | ## Build Pipeline
35 |
36 | ### Complete Build Validation
37 |
38 | ```bash
39 | npm run format && npm run lint && npm run build:lib && npm run test:lib
40 | ```
41 |
42 | **Execution time**: ~45 seconds
43 | **Timeout**: Use 120+ seconds minimum, NEVER CANCEL
44 |
45 | ### Individual Commands
46 |
47 | ```bash
48 | # Code Quality
49 | npm run format # Check formatting (5s)
50 | npm run format:write # Fix formatting (5s)
51 | npm run lint # Lint all projects (15s)
52 |
53 | # Library Build
54 | npm run build:lib # Production build (20s)
55 | npm run watch:lib # Development watch mode
56 |
57 | # Testing
58 | npm run test:lib # Unit tests with coverage (15s)
59 | npm run e2e:run # E2E tests headless (60s+)
60 |
61 | # Development Servers
62 | npm run serve:demo # Demo app - localhost:4200
63 | npm run serve:example # Example app - localhost:4300
64 | ```
65 |
66 | ## Angular v20+ Development Standards
67 |
68 | ### Core Principles
69 |
70 | **Signal-First Architecture**: Use signals as the primary state management pattern. Observables are reserved for streams and HTTP operations only.
71 |
72 | **Component Design**: All components MUST be standalone with OnPush change detection and signal-based APIs.
73 |
74 | **Template Syntax**: Exclusive use of modern control flow (`@if`, `@for`, `@switch`) and direct binding patterns.
75 |
76 | ### TypeScript Configuration
77 |
78 | ```typescript
79 | // Use strict TypeScript with latest features
80 | {
81 | "compilerOptions": {
82 | "strict": true,
83 | "exactOptionalPropertyTypes": true,
84 | "noUncheckedIndexedAccess": true,
85 | "noImplicitReturns": true,
86 | "noFallthroughCasesInSwitch": true
87 | }
88 | }
89 | ```
90 |
91 | **Type Standards**:
92 |
93 | - NEVER use `any` - use `unknown` for uncertain types
94 | - Use `satisfies` operator for type checking with inference
95 | - Prefer `readonly` for all data that shouldn't be mutated
96 | - Use template literal types for string constants
97 |
98 | ### Component Architecture
99 |
100 | ```typescript
101 | import {
102 | ChangeDetectionStrategy,
103 | Component,
104 | computed,
105 | effect,
106 | inject,
107 | input,
108 | output,
109 | signal,
110 | ElementRef,
111 | } from '@angular/core';
112 |
113 | @Component({
114 | selector: 'viewport-element',
115 | standalone: true,
116 | changeDetection: ChangeDetectionStrategy.OnPush,
117 | template: `
118 | @if (isInViewport()) {
119 |
120 | Content is visible ({{ visibilityRatio() }}%)
121 |
122 | } @else {
123 | Content is hidden
124 | }
125 | `,
126 | host: {
127 | '[attr.data-viewport-state]': 'viewportState()',
128 | '[class.in-viewport]': 'isInViewport()',
129 | },
130 | })
131 | export class ViewportElementComponent {
132 | // Signal inputs - primary pattern for v20+
133 | readonly threshold = input(0.5);
134 | readonly rootMargin = input('0px');
135 | readonly trackVisibility = input(true);
136 |
137 | // Signal outputs - modern event handling
138 | readonly visibilityChange = output<{
139 | isVisible: boolean;
140 | entry: IntersectionObserverEntry;
141 | }>();
142 |
143 | // Dependency injection with inject()
144 | private readonly elementRef = inject(ElementRef);
145 | private readonly viewportService = inject(ViewportService);
146 |
147 | // Internal signals
148 | protected readonly isInViewport = signal(false);
149 | protected readonly visibilityRatio = signal(0);
150 | private readonly lastEntry = signal(null);
151 |
152 | // Computed values - derived state
153 | protected readonly opacity = computed(
154 | () => this.visibilityRatio() * 0.8 + 0.2
155 | );
156 |
157 | protected readonly viewportState = computed(() =>
158 | this.isInViewport() ? 'visible' : 'hidden'
159 | );
160 |
161 | constructor() {
162 | // Effects for side effects and reactions
163 | effect(() => {
164 | if (this.trackVisibility()) {
165 | this.setupViewportObserver();
166 | }
167 | });
168 |
169 | effect(() => {
170 | // Emit events when visibility changes
171 | const entry = this.lastEntry();
172 | if (entry) {
173 | this.visibilityChange.emit({
174 | isVisible: this.isInViewport(),
175 | entry,
176 | });
177 | }
178 | });
179 | }
180 |
181 | private setupViewportObserver(): void {
182 | // Implementation with Intersection Observer
183 | this.viewportService.observe(this.elementRef.nativeElement, {
184 | threshold: this.threshold(),
185 | rootMargin: this.rootMargin(),
186 | callback: (entry) => {
187 | this.isInViewport.set(entry.isIntersecting);
188 | this.visibilityRatio.set(entry.intersectionRatio * 100);
189 | this.lastEntry.set(entry);
190 | },
191 | });
192 | }
193 | }
194 | ```
195 |
196 | ### Service Design Patterns
197 |
198 | ```typescript
199 | import { Injectable, inject, signal, computed } from '@angular/core';
200 | import { DOCUMENT } from '@angular/common';
201 | import { isPlatformBrowser } from '@angular/common';
202 | import { PLATFORM_ID } from '@angular/core';
203 |
204 | interface ViewportConfig {
205 | threshold: number | number[];
206 | rootMargin: string;
207 | callback: (entry: IntersectionObserverEntry) => void;
208 | }
209 |
210 | interface ViewportGlobalConfig {
211 | rootMargin: string;
212 | threshold: number[];
213 | }
214 |
215 | @Injectable({ providedIn: 'root' })
216 | export class ViewportService {
217 | private readonly document = inject(DOCUMENT);
218 | private readonly platformId = inject(PLATFORM_ID);
219 |
220 | // Signal-based state management
221 | private readonly _elements = signal