- pnpm dlx @angular/cli@latest new APP_NAME -s -t --routing --package-manager=pnpm --ssr
- pnpm add -D @spartan-ng/cli
- pnpm add @angular/cdk @spartan-ng/ui-core
- pnpm add -D tailwindcss postcss autoprefixer
- pnpm dlx tailwindcss init
tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
presets: [require('@spartan-ng/ui-core/hlm-tailwind-preset')],
content: [
'./src/**/*.{html,ts}',
'./REPLACE_WITH_PATH_TO_YOUR_COMPONENTS_DIRECTORY/**/*.{html,ts}',
],
theme: {
extend: {},
},
plugins: [],
};
REPLACE_WITH_PATH_TO_YOUR_COMPONENTS_DIRECTORY => libs/ui
styles.css
@tailwind base;
@tailwind components;
@tailwind utilities;
- pnpm ng g @spartan-ng/cli:ui-theme @spartan-ng/cli:ui
- pnpm ng add ngxtension
- pnpm add @tanstack/angular-table @tanstack/angular-query-experimental
- pnpm add @tanstack/angular-form @tanstack/zod-form-adapter zod
Final style.css
@import "@angular/cdk/overlay-prebuilt.css";
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
--font-sans: "";
}
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--ring: 222.2 84% 4.9%;
--radius: 0.5rem;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0, 91%, 71%;
--destructive-foreground: 210 40% 98%;
--ring: 212.7 26.8% 83.9;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}
angular.json Add necessary settings
"schematics": {
"@schematics/angular:component": {
"inlineTemplate": true,
"inlineStyle": true,
"standalone": true,
"changeDetection": "OnPush"
}
}
- pnpm ng add @angular-eslint/schematics
package.json
scripts: {
"lint": "ng lint",
"lint:fix": "ng lint --fix"
"prettier:check": "prettier --check ./src",
"prettier:write": "prettier --write ./src"
}
eslint.config.json
// @ts-check
const eslint = require('@eslint/js');
const tseslint = require('typescript-eslint');
const angular = require('angular-eslint');
module.exports = tseslint.config(
{
files: ['**/*.ts'],
extends: [
eslint.configs.recommended,
...tseslint.configs.recommended,
...tseslint.configs.stylistic,
...angular.configs.tsRecommended,
],
processor: angular.processInlineTemplates,
rules: {
'@angular-eslint/directive-selector': [
'error',
{
type: 'attribute',
prefix: 'app',
style: 'camelCase',
},
],
'@angular-eslint/component-selector': [
'error',
{
type: 'element',
prefix: 'app',
style: 'kebab-case',
},
],
'@typescript-eslint/no-unused-vars': ['warn'],
},
},
{
files: ['**/*.html'],
extends: [
...angular.configs.templateRecommended,
...angular.configs.templateAccessibility,
],
rules: {
'prettier/prettier': [
'error',
{
parser: 'angular',
},
],
},
}
);
-
pnpm add -D prettier@latest
-
touch .prettierignore
# See http://help.github.com/ignore-files/ for more about ignoring files.
# Compiled output
/dist
/tmp
/out-tsc
/bazel-out
# Node
/node_modules
npm-debug.log
yarn-error.log
# IDEs and editors
.idea/
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# Visual Studio Code
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.history/*
# Miscellaneous
/.angular/cache
.sass-cache/
/connect.lock
/coverage
/libpeerconnection.log
testem.log
/typings
# System files
.DS_Store
Thumbs.db
libs/ui
- touch .prettierrc.json
{
"tabWidth": 2,
"useTabs": false,
"singleQuote": true,
"semi": true,
"bracketSpacing": true,
"arrowParens": "avoid",
"trailingComma": "es5",
"bracketSameLine": true,
"printWidth": 80,
"endOfLine": "lf"
}
app.component.ts - and change detection strategy
@Component({
selector: 'app-root',
standalone: true,
imports: [ RouterOutlet ],
host: {
class: ''
},
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<router-outlet />
`,
styles: [],
})
index.html - add script to choose theme in <head>
tag
<script>
if (
// check if user had saved dark as their
// theme when accessing page before
localStorage.theme === "dark" ||
// or user's requesting dark color
// scheme through operating system
(!("theme" in localStorage) &&
window.matchMedia("(prefers-color-scheme: dark)").matches)
) {
// then if we have access to the document and the element
// we add the dark class to the html element and
// store the dark value in the localStorage
if (document && document.documentElement) {
document.documentElement.classList.add("dark");
localStorage.setItem("theme", "dark");
}
} else {
// else if we have access to the document and the element
// we remove the dark class to the html element and
// store the value light in the localStorage
if (document && document.documentElement) {
document.documentElement.classList.remove("dark");
localStorage.setItem("theme", "light");
}
}
</script>
theme.type.ts Type for the theme
export type Theme = 'light' | 'dark';
theme.service.ts - Theme service
import { DOCUMENT, isPlatformBrowser } from '@angular/common';
import { injectDestroy } from 'ngxtension/inject-destroy';
import {
Injectable,
PLATFORM_ID,
RendererFactory2,
inject,
} from '@angular/core';
import { ReplaySubject, takeUntil } from 'rxjs';
import { Theme } from '@/types/theme';
@Injectable({
providedIn: 'root',
})
export class ThemeService {
private THEME = 'theme';
private _platformId = inject(PLATFORM_ID);
private _renderer = inject(RendererFactory2).createRenderer(null, null);
private _document = inject(DOCUMENT);
private _theme = new ReplaySubject<Theme>(1);
public theme$ = this._theme.asObservable();
private _destroy$ = injectDestroy();
constructor() {
this._syncThemeFromLocalStorage();
this._toggleClassOnThemeChanges();
}
private _syncThemeFromLocalStorage(): void {
if (isPlatformBrowser(this._platformId)) {
this._theme.next(
localStorage.getItem(this.THEME) === 'dark' ? 'dark' : 'light'
);
}
}
private _toggleClassOnThemeChanges(): void {
this.theme$.pipe(takeUntil(this._destroy$)).subscribe((theme) => {
if (theme === 'dark') {
this._renderer.addClass(this._document.documentElement, 'dark');
} else {
if (this._document.documentElement.className.includes('dark')) {
this._renderer.removeClass(this._document.documentElement, 'dark');
}
}
});
}
public setTheme(theme: Theme) {
localStorage.setItem(this.THEME, theme);
this._theme.next(theme);
}
}