diff --git a/.vscode/settings.json b/.vscode/settings.json index 72dbca1b..fb1e10aa 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,7 +1,11 @@ { - "java.compile.nullAnalysis.mode": "automatic", - "java.configuration.updateBuildConfiguration": "interactive", - "editor.codeActionsOnSave": { - "source.fixAll": true - } -} \ No newline at end of file + "java.compile.nullAnalysis.mode": "automatic", + "java.configuration.updateBuildConfiguration": "interactive", + "tailwindCSS.experimental.classRegex": [ + "cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]", + "cn\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]" + ], + "editor.codeActionsOnSave": { + "source.fixAll": true + } +} diff --git a/webapp/eslint.config.js b/webapp/eslint.config.js index 5329136c..3ecbff00 100644 --- a/webapp/eslint.config.js +++ b/webapp/eslint.config.js @@ -8,6 +8,7 @@ module.exports = [ { ignores: [ '.cache/', + '.angular/', '.git/', '.github/', 'build/', @@ -54,10 +55,7 @@ module.exports = [ style: 'kebab-case', }, ], - '@angular-eslint/prefer-standalone': 'error', - '@angular-eslint/template/prefer-ngsrc': 'error', - '@angular-eslint/template/prefer-self-closing-tags': 'error', - '@angular-eslint/template/prefer-control-flow': 'error', + '@typescript-eslint/no-empty-object-type': 'off', }, }, { diff --git a/webapp/src/app/app.component.html b/webapp/src/app/app.component.html index 55ae350d..460bba46 100644 --- a/webapp/src/app/app.component.html +++ b/webapp/src/app/app.component.html @@ -3,9 +3,9 @@

Header

- - + +
- + - \ No newline at end of file + diff --git a/webapp/src/app/app.component.ts b/webapp/src/app/app.component.ts index 3bf1d72a..513811ab 100644 --- a/webapp/src/app/app.component.ts +++ b/webapp/src/app/app.component.ts @@ -8,7 +8,7 @@ import { HelloComponent } from './example/hello/hello.component'; standalone: true, imports: [RouterOutlet, CounterComponent, HelloComponent], templateUrl: './app.component.html', - styles: [], + styles: [] }) export class AppComponent { title = 'Hephaestus'; diff --git a/webapp/src/app/app.config.ts b/webapp/src/app/app.config.ts index fc69a29e..c3b646d2 100644 --- a/webapp/src/app/app.config.ts +++ b/webapp/src/app/app.config.ts @@ -1,22 +1,18 @@ import { ApplicationConfig, importProvidersFrom, provideExperimentalZonelessChangeDetection } from '@angular/core'; import { provideRouter } from '@angular/router'; import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; -import { - provideAngularQuery, - QueryClient, -} from '@tanstack/angular-query-experimental' -import {LucideAngularModule, Home } from "lucide-angular"; +import { provideAngularQuery, QueryClient } from '@tanstack/angular-query-experimental'; +import { LucideAngularModule, Home } from 'lucide-angular'; import { routes } from './app.routes'; import { BASE_PATH } from './core/modules/openapi'; - export const appConfig: ApplicationConfig = { providers: [ provideExperimentalZonelessChangeDetection(), provideRouter(routes), provideAngularQuery(new QueryClient()), - { provide: BASE_PATH, useValue: "http://localhost:8080" }, + { provide: BASE_PATH, useValue: 'http://localhost:8080' }, provideHttpClient(withInterceptorsFromDi()), importProvidersFrom(LucideAngularModule.pick({ Home })) ] -}; \ No newline at end of file +}; diff --git a/webapp/src/app/example/counter/counter.component.html b/webapp/src/app/example/counter/counter.component.html index 87e73a15..9c3d46a6 100644 --- a/webapp/src/app/example/counter/counter.component.html +++ b/webapp/src/app/example/counter/counter.component.html @@ -1,17 +1,21 @@
-

Counter Title: {{title()}}

-

Value: {{counter()}}

-

HexValue: {{hexCounter()}}

- - Increment - @if (byCount() > 1) { - by {{byCount()}} - } - +

Counter Title: {{ title() }}

+

Value: {{ counter() }}

+

+

HexValue: {{ hexCounter() }}

+

+ + Increment + @if (byCount() > 1) { + by {{ byCount() }} + } + +

+

History

@for (item of counterHistory(); track item.dec) { {{ item.dec }} }
-
\ No newline at end of file + diff --git a/webapp/src/app/example/counter/counter.component.ts b/webapp/src/app/example/counter/counter.component.ts index fb2eb490..0b3fb872 100644 --- a/webapp/src/app/example/counter/counter.component.ts +++ b/webapp/src/app/example/counter/counter.component.ts @@ -8,10 +8,10 @@ interface CounterHistoryEntry { } @Component({ - selector: 'counter', + selector: 'app-counter', standalone: true, imports: [AppButtonComponent], - templateUrl: './counter.component.html', + templateUrl: './counter.component.html' }) export class CounterComponent { // we put all our application data inside signals! -> most optimal change detection and re-rendering possible @@ -26,9 +26,9 @@ export class CounterComponent { // Pitfall: conditional logic inside computed // When calling computed initially the dependency signal has to be called, otherwise it will not work return this.counter().toString(16); - }) + }); - counterHistory = signal([{ dec: 0, hex: "0" }]) + counterHistory = signal([{ dec: 0, hex: '0' }]); constructor() { console.log(`counter value: ${this.counter()}`); @@ -36,7 +36,8 @@ export class CounterComponent { // `effect`s goes in constructor (?) // `effectRef` is not necessary, only if needed // Runs once when the effect is declared to collect dependencies, and again when they change - const effectRef = effect((onCleanup) => { + // const effectRef = + effect((onCleanup) => { const currentCount = this.counter(); const currentHexCount = this.hexCounter(); @@ -46,8 +47,8 @@ export class CounterComponent { onCleanup(() => { console.log('Perform cleanup action here'); - }) - }) + }); + }); // effectRef.destroy() at any time! Usually not necessary @@ -57,9 +58,9 @@ export class CounterComponent { increment() { console.log('Updating counter...'); - this.counter.update(counter => counter + this.byCount()); + this.counter.update((counter) => counter + this.byCount()); // Update values of a signal only through Signals API, e.g.`set()` and `update()`, not directly (i.e. `push()`) - this.counterHistory.update(history => [...history, { dec: this.counter(), hex: this.hexCounter() }]) + this.counterHistory.update((history) => [...history, { dec: this.counter(), hex: this.hexCounter() }]); } } diff --git a/webapp/src/app/example/counter/counter.stories.ts b/webapp/src/app/example/counter/counter.stories.ts index 8f8504b5..a751c49e 100644 --- a/webapp/src/app/example/counter/counter.stories.ts +++ b/webapp/src/app/example/counter/counter.stories.ts @@ -8,9 +8,9 @@ const meta: Meta = { component: CounterComponent, tags: ['autodocs'], argTypes: toArgs({ - title: "test", - byCount: 2, - }), + title: 'test', + byCount: 2 + }) }; export default meta; @@ -21,5 +21,5 @@ export const Primary: Story = { args: { title: 'Counter', byCount: 2 - }, -}; \ No newline at end of file + } +}; diff --git a/webapp/src/app/example/counter/counter.ts b/webapp/src/app/example/counter/counter.ts index a16ed0a6..93cf9c2b 100644 --- a/webapp/src/app/example/counter/counter.ts +++ b/webapp/src/app/example/counter/counter.ts @@ -1,4 +1,4 @@ -import { signal } from "@angular/core"; +import { signal } from '@angular/core'; export const counter = signal(0); @@ -18,9 +18,9 @@ export const counter = signal(0); // // inject any dependencies you need here // } // -// // anyone needing to modify the signal +// // anyone needing to modify the signal // // needs to do so in a controlled way // incrementCounter() { // this.counterSignal.update((val) => val + 1); // } -// } \ No newline at end of file +// } diff --git a/webapp/src/app/example/hello/hello.component.ts b/webapp/src/app/example/hello/hello.component.ts index 30c42c61..12a35298 100644 --- a/webapp/src/app/example/hello/hello.component.ts +++ b/webapp/src/app/example/hello/hello.component.ts @@ -1,36 +1,34 @@ import { Component, inject } from '@angular/core'; import { injectMutation, injectQuery, injectQueryClient } from '@tanstack/angular-query-experimental'; -import { AngularQueryDevtools } from '@tanstack/angular-query-devtools-experimental' +import { AngularQueryDevtools } from '@tanstack/angular-query-devtools-experimental'; import { lastValueFrom } from 'rxjs'; import { HelloService } from 'app/core/modules/openapi'; import { AppButtonComponent } from 'app/ui/button/button/button.component'; @Component({ - selector: 'hello', + selector: 'app-hello', standalone: true, imports: [AppButtonComponent, AngularQueryDevtools], templateUrl: './hello.component.html' }) export class HelloComponent { - helloService = inject(HelloService) - queryClient = injectQueryClient() + helloService = inject(HelloService); + queryClient = injectQueryClient(); query = injectQuery(() => ({ queryKey: ['hellos'], - queryFn: async () => lastValueFrom(this.helloService.getAllHellos()), - }) - ); + queryFn: async () => lastValueFrom(this.helloService.getAllHellos()) + })); mutation = injectMutation(() => ({ mutationFn: () => lastValueFrom(this.helloService.addHello()), - onSuccess: () => + onSuccess: () => this.queryClient.invalidateQueries({ - queryKey: ['hellos'], - }), - }) - ); + queryKey: ['hellos'] + }) + })); addHello() { this.mutation.mutate(); } -} \ No newline at end of file +} diff --git a/webapp/src/app/storybook.helper.ts b/webapp/src/app/storybook.helper.ts index a269d051..49322ae7 100644 --- a/webapp/src/app/storybook.helper.ts +++ b/webapp/src/app/storybook.helper.ts @@ -1,13 +1,10 @@ import { InputSignalWithTransform, InputSignal, EventEmitter } from '@angular/core'; -import { Args, ArgTypes } from '@storybook/angular'; import { cva as CVA } from 'class-variance-authority'; import { ClassProp, ClassValue, StringToBoolean } from 'class-variance-authority/types'; // Source: // https://stackoverflow.com/questions/78379300/how-do-i-use-angular-input-signals-with-storybook -export function toArgs( - args: Partial>> -): TransformEventType { +export function toArgs(args: Partial>>): TransformEventType { return args as unknown as TransformEventType; } @@ -22,28 +19,23 @@ type TransformSignalInputType = { }; // Type to extract the type from InputSignal or InputSignalWithTransform -type TransformInputType = - T extends InputSignalWithTransform - ? U - : T extends InputSignal - ? U - : T; - - +type TransformInputType = T extends InputSignalWithTransform ? U : T extends InputSignal ? U : T; // CVA Storybook Helper type ConfigSchema = Record>; type ConfigVariants = { - [Variant in keyof T]?: StringToBoolean | null | undefined; + [Variant in keyof T]?: StringToBoolean | null | undefined; }; type ConfigVariantsMulti = { - [Variant in keyof T]?: StringToBoolean | StringToBoolean[] | undefined; + [Variant in keyof T]?: StringToBoolean | StringToBoolean[] | undefined; }; -type Config = T extends ConfigSchema ? { - variants?: T; - defaultVariants?: ConfigVariants; - compoundVariants?: (T extends ConfigSchema ? (ConfigVariants | ConfigVariantsMulti) & ClassProp : ClassProp)[]; -} : never; +type Config = T extends ConfigSchema + ? { + variants?: T; + defaultVariants?: ConfigVariants; + compoundVariants?: (T extends ConfigSchema ? (ConfigVariants | ConfigVariantsMulti) & ClassProp : ClassProp)[]; + } + : never; function createCVAArgTypes(config?: Config) { if (!config?.variants) { @@ -53,12 +45,14 @@ function createCVAArgTypes(config?: Config) { const variants = config?.variants; return Object.fromEntries( Object.entries(variants).map(([variant, options]) => { - const variantKey = variant as keyof T; - const optionsArray = Object.keys(options) as (keyof T[typeof variantKey])[]; - return [variant, { - control: { type: 'select' }, - options: optionsArray - }]; + const optionsArray = Object.keys(options) as (keyof T[typeof variant])[]; + return [ + variant, + { + control: { type: 'select' }, + options: optionsArray + } + ]; }) ); } @@ -66,7 +60,7 @@ function createCVAArgTypes(config?: Config) { function createCVADefaultArgs(config?: Config) { return (config?.defaultVariants || {}) as { [Variant in keyof T]: keyof T[Variant]; - } + }; } export function cva(base?: ClassValue, config?: Config) { diff --git a/webapp/src/app/ui/button/button/button.component.html b/webapp/src/app/ui/button/button/button.component.html index 8677d1db..57c21bac 100644 --- a/webapp/src/app/ui/button/button/button.component.html +++ b/webapp/src/app/ui/button/button/button.component.html @@ -1,3 +1,3 @@ \ No newline at end of file + diff --git a/webapp/src/app/ui/button/button/button.component.ts b/webapp/src/app/ui/button/button/button.component.ts index 8d3a7235..fa921b14 100644 --- a/webapp/src/app/ui/button/button/button.component.ts +++ b/webapp/src/app/ui/button/button/button.component.ts @@ -14,19 +14,19 @@ const [buttonVariants, args, argTypes] = cva( outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground', secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80', ghost: 'hover:bg-accent hover:text-accent-foreground', - link: 'text-primary underline-offset-4 hover:underline', + link: 'text-primary underline-offset-4 hover:underline' }, size: { default: 'h-10 px-4 py-2', sm: 'h-9 rounded-md px-3', lg: 'h-11 rounded-md px-8', - icon: 'h-10 w-10', - }, + icon: 'h-10 w-10' + } }, defaultVariants: { variant: 'default', - size: 'default', - }, + size: 'default' + } } ); @@ -37,10 +37,10 @@ interface ButtonVariants extends VariantProps {} @Component({ selector: 'app-button', standalone: true, - templateUrl: './button.component.html', + templateUrl: './button.component.html' }) export class AppButtonComponent { - class = input(''); + class = input(''); variant = input('default'); size = input('default'); disabled = input(false); @@ -48,4 +48,4 @@ export class AppButtonComponent { onClick = output(); computedClass = computed(() => cn(buttonVariants({ variant: this.variant(), size: this.size() }), this.class())); -} \ No newline at end of file +} diff --git a/webapp/src/app/ui/button/button/button.stories.ts b/webapp/src/app/ui/button/button/button.stories.ts index 2c7c7158..a3b254cb 100644 --- a/webapp/src/app/ui/button/button/button.stories.ts +++ b/webapp/src/app/ui/button/button/button.stories.ts @@ -11,17 +11,17 @@ const meta: Meta = { args: { ...args, disabled: false, - onClick: fn(), + onClick: fn() }, argTypes: { ...argTypes, disabled: { - control: 'boolean', + control: 'boolean' }, onClick: { - action: 'onClick', - }, - }, + action: 'onClick' + } + } }; export default meta; @@ -36,17 +36,17 @@ export const Primary: Story = { }, render: (args) => ({ props: args, - template: `Primary`, + template: `Primary` }) }; export const Secondary: Story = { args: { - variant: "secondary", - size: "default" + variant: 'secondary', + size: 'default' }, - render: args => ({ + render: (args) => ({ props: args, template: `Secondary` }) @@ -54,11 +54,11 @@ export const Secondary: Story = { export const Destructive: Story = { args: { - variant: "destructive", - size: "default" + variant: 'destructive', + size: 'default' }, - render: args => ({ + render: (args) => ({ props: args, template: `Destructive` }) @@ -66,11 +66,11 @@ export const Destructive: Story = { export const Outline: Story = { args: { - variant: "outline", - size: "default" + variant: 'outline', + size: 'default' }, - render: args => ({ + render: (args) => ({ props: args, template: `Outline` }) @@ -78,11 +78,11 @@ export const Outline: Story = { export const Ghost: Story = { args: { - variant: "ghost", - size: "default" + variant: 'ghost', + size: 'default' }, - render: args => ({ + render: (args) => ({ props: args, template: `Ghost` }) @@ -90,11 +90,11 @@ export const Ghost: Story = { export const Link: Story = { args: { - variant: "link", - size: "default" + variant: 'link', + size: 'default' }, - render: args => ({ + render: (args) => ({ props: args, template: `Link` }) @@ -103,45 +103,45 @@ export const Link: Story = { export const Icon: Story = { decorators: [ moduleMetadata({ - imports: [LucideAngularModule.pick({ ChevronRight })], - }), + imports: [LucideAngularModule.pick({ ChevronRight })] + }) ], render: (args) => ({ props: { variant: 'outline', - size: 'icon', + size: 'icon' }, - template: ``, + template: `` }) }; export const WithIcon: Story = { decorators: [ moduleMetadata({ - imports: [LucideAngularModule.pick({ Mail })], - }), + imports: [LucideAngularModule.pick({ Mail })] + }) ], render: (args) => ({ props: { variant: 'default', - size: 'default', + size: 'default' }, - template: `Login with Email`, + template: `Login with Email` }) }; export const Loading: Story = { decorators: [ moduleMetadata({ - imports: [LucideAngularModule.pick({ Loader2 })], - }), + imports: [LucideAngularModule.pick({ Loader2 })] + }) ], render: (args) => ({ props: { variant: 'default', size: 'default', - disabled: true, + disabled: true }, - template: `Please wait`, + template: `Please wait` }) }; diff --git a/webapp/src/app/ui/input/input.component.html b/webapp/src/app/ui/input/input.component.html new file mode 100644 index 00000000..6b64b478 --- /dev/null +++ b/webapp/src/app/ui/input/input.component.html @@ -0,0 +1 @@ + diff --git a/webapp/src/app/ui/input/input.component.ts b/webapp/src/app/ui/input/input.component.ts new file mode 100644 index 00000000..bd46e268 --- /dev/null +++ b/webapp/src/app/ui/input/input.component.ts @@ -0,0 +1,49 @@ +import { Component, computed, input, output } from '@angular/core'; +import type { ClassValue } from 'clsx'; +import { VariantProps } from 'class-variance-authority'; +import { cn } from 'app/utils'; +import { cva } from 'app/storybook.helper'; + +const [inputVariants, args, argTypes] = cva( + 'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50', + { + variants: { + size: { + default: 'h-10 px-4 py-2', + sm: 'h-9 px-2 py-1', + lg: 'h-11 px-4 py-3' + } + }, + defaultVariants: { + size: 'default' + } + } +); + +export { args, argTypes }; + +interface InputVariants extends VariantProps {} + +@Component({ + selector: 'app-input', + standalone: true, + templateUrl: './input.component.html' +}) +export class AppInputComponent { + class = input(''); + type = input('text'); + placeholder = input(''); + size = input('default'); + disabled = input(false); + value = input(''); + id = input(''); + + valueChange = output(); + + onInput(event: Event) { + const inputValue = (event.target as HTMLInputElement).value; + this.valueChange.emit(inputValue); + } + + computedClass = computed(() => cn(inputVariants({ size: this.size() }), this.class())); +} diff --git a/webapp/src/app/ui/input/input.stories.ts b/webapp/src/app/ui/input/input.stories.ts new file mode 100644 index 00000000..3ce17104 --- /dev/null +++ b/webapp/src/app/ui/input/input.stories.ts @@ -0,0 +1,81 @@ +import { argsToTemplate, moduleMetadata, type Meta, type StoryObj } from '@storybook/angular'; +import { AppInputComponent, args, argTypes } from './input.component'; +import { action } from '@storybook/addon-actions'; +import { AppButtonComponent } from '@app/ui/button/button/button.component'; +import { AppLabelComponent } from '@app/ui/label/label.component'; + +const meta: Meta = { + title: 'UI/Input', + component: AppInputComponent, + tags: ['autodocs'], + args: { + ...args, + value: '', + disabled: false, + size: 'default' + }, + argTypes: { + ...argTypes, + disabled: { + control: 'boolean' + }, + onInput: { + action: 'onInput' + } + }, + decorators: [ + moduleMetadata({ + imports: [AppButtonComponent, AppLabelComponent] + }) + ] +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + render: (args) => ({ + props: args, + template: `` + }) +}; + +export const Disabled: Story = { + args: { + disabled: true + }, + render: (args) => ({ + props: args, + template: `` + }) +}; + +export const WithLabel: Story = { + render: (args) => ({ + props: args, + template: ` +
+ Label + +
+ ` + }) +}; + +export const WithButton: Story = { + render: (args) => ({ + props: { + args, + userInput: '', + onButtonClick(value: string) { + action('Button Clicked')(`Input Value: ${value}`); + } + }, + template: ` +
+ + Submit +
+ ` + }) +}; diff --git a/webapp/src/app/ui/label/label.component.html b/webapp/src/app/ui/label/label.component.html new file mode 100644 index 00000000..72d84052 --- /dev/null +++ b/webapp/src/app/ui/label/label.component.html @@ -0,0 +1,3 @@ + diff --git a/webapp/src/app/ui/label/label.component.ts b/webapp/src/app/ui/label/label.component.ts new file mode 100644 index 00000000..dcd21155 --- /dev/null +++ b/webapp/src/app/ui/label/label.component.ts @@ -0,0 +1,20 @@ +import { Component, computed, input } from '@angular/core'; +import type { ClassValue } from 'clsx'; +import { cn } from 'app/utils'; +import { cva } from 'app/storybook.helper'; + +const [labelVariants, args, argTypes] = cva('text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70'); + +export { args, argTypes }; + +@Component({ + selector: 'app-label', + standalone: true, + templateUrl: './label.component.html' +}) +export class AppLabelComponent { + class = input(''); + for = input(''); + + computedClass = computed(() => cn(labelVariants({}), this.class())); +} diff --git a/webapp/src/app/ui/label/label.stories.ts b/webapp/src/app/ui/label/label.stories.ts new file mode 100644 index 00000000..41247915 --- /dev/null +++ b/webapp/src/app/ui/label/label.stories.ts @@ -0,0 +1,25 @@ +import { argsToTemplate, type Meta, type StoryObj } from '@storybook/angular'; +import { AppLabelComponent, args, argTypes } from './label.component'; + +const meta: Meta = { + title: 'UI/Label', + component: AppLabelComponent, + tags: ['autodocs'], + args: { + ...args, + for: 'example-input' + }, + argTypes: { + ...argTypes + } +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + render: (args) => ({ + props: args, + template: `Label` + }) +}; diff --git a/webapp/src/app/utils.ts b/webapp/src/app/utils.ts index d084ccad..9ad0df42 100644 --- a/webapp/src/app/utils.ts +++ b/webapp/src/app/utils.ts @@ -1,6 +1,6 @@ -import { type ClassValue, clsx } from "clsx" -import { twMerge } from "tailwind-merge" +import { type ClassValue, clsx } from 'clsx'; +import { twMerge } from 'tailwind-merge'; export function cn(...inputs: ClassValue[]) { - return twMerge(clsx(inputs)) + return twMerge(clsx(inputs)); } diff --git a/webapp/src/index.html b/webapp/src/index.html index c4c981e0..755019f9 100644 --- a/webapp/src/index.html +++ b/webapp/src/index.html @@ -1,13 +1,13 @@ - - - Hephaestus - - - - - - - + + + Hephaestus + + + + + + + diff --git a/webapp/src/main.ts b/webapp/src/main.ts index 35b00f34..17447a5d 100644 --- a/webapp/src/main.ts +++ b/webapp/src/main.ts @@ -2,5 +2,4 @@ import { bootstrapApplication } from '@angular/platform-browser'; import { appConfig } from './app/app.config'; import { AppComponent } from './app/app.component'; -bootstrapApplication(AppComponent, appConfig) - .catch((err) => console.error(err)); +bootstrapApplication(AppComponent, appConfig).catch((err) => console.error(err)); diff --git a/webapp/src/styles.css b/webapp/src/styles.css index 33aec53d..00b08e39 100644 --- a/webapp/src/styles.css +++ b/webapp/src/styles.css @@ -56,4 +56,4 @@ body { @apply bg-background text-foreground; } -} \ No newline at end of file +}