@gob-ds/gob-ds 1.0.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 (84) hide show
  1. package/.editorconfig +17 -0
  2. package/.storybook/main.ts +19 -0
  3. package/.storybook/preview.ts +18 -0
  4. package/.storybook/tsconfig.doc.json +10 -0
  5. package/.storybook/tsconfig.json +11 -0
  6. package/.storybook/typings.d.ts +4 -0
  7. package/.vscode/extensions.json +4 -0
  8. package/.vscode/launch.json +20 -0
  9. package/.vscode/mcp.json +9 -0
  10. package/.vscode/tasks.json +42 -0
  11. package/README.md +0 -0
  12. package/angular.json +113 -0
  13. package/documentation.json +1472 -0
  14. package/package.json +65 -0
  15. package/public/favicon.ico +0 -0
  16. package/src/app/app.config.server.ts +12 -0
  17. package/src/app/app.config.ts +12 -0
  18. package/src/app/app.html +342 -0
  19. package/src/app/app.routes.server.ts +8 -0
  20. package/src/app/app.routes.ts +3 -0
  21. package/src/app/app.scss +0 -0
  22. package/src/app/app.spec.ts +23 -0
  23. package/src/app/app.ts +12 -0
  24. package/src/app/lib/alert-dialog/alert-dialog.component.html +35 -0
  25. package/src/app/lib/alert-dialog/alert-dialog.component.scss +94 -0
  26. package/src/app/lib/alert-dialog/alert-dialog.component.ts +144 -0
  27. package/src/app/lib/badge/badge.component.ts +25 -0
  28. package/src/app/lib/badge/badge.scss +50 -0
  29. package/src/app/lib/button/button.component.html +35 -0
  30. package/src/app/lib/button/button.component.scss +226 -0
  31. package/src/app/lib/button/button.component.ts +70 -0
  32. package/src/app/lib/checkbox/checkbox.component.html +34 -0
  33. package/src/app/lib/checkbox/checkbox.component.scss +80 -0
  34. package/src/app/lib/checkbox/checkbox.component.ts +84 -0
  35. package/src/app/lib/input/input.component.html +43 -0
  36. package/src/app/lib/input/input.component.scss +181 -0
  37. package/src/app/lib/input/input.component.ts +87 -0
  38. package/src/app/lib/search/search.component.html +30 -0
  39. package/src/app/lib/search/search.component.scss +102 -0
  40. package/src/app/lib/search/search.component.ts +73 -0
  41. package/src/index.html +13 -0
  42. package/src/main.server.ts +8 -0
  43. package/src/main.ts +6 -0
  44. package/src/server.ts +68 -0
  45. package/src/stories/Configure.mdx +364 -0
  46. package/src/stories/assets/accessibility.png +0 -0
  47. package/src/stories/assets/accessibility.svg +1 -0
  48. package/src/stories/assets/addon-library.png +0 -0
  49. package/src/stories/assets/assets.png +0 -0
  50. package/src/stories/assets/avif-test-image.avif +0 -0
  51. package/src/stories/assets/context.png +0 -0
  52. package/src/stories/assets/discord.svg +1 -0
  53. package/src/stories/assets/docs.png +0 -0
  54. package/src/stories/assets/figma-plugin.png +0 -0
  55. package/src/stories/assets/github.svg +1 -0
  56. package/src/stories/assets/share.png +0 -0
  57. package/src/stories/assets/styling.png +0 -0
  58. package/src/stories/assets/testing.png +0 -0
  59. package/src/stories/assets/theming.png +0 -0
  60. package/src/stories/assets/tutorials.svg +1 -0
  61. package/src/stories/assets/youtube.svg +1 -0
  62. package/src/stories/components/alert-dialog.stories.ts +60 -0
  63. package/src/stories/components/badge.stories.ts +111 -0
  64. package/src/stories/components/button.stories.ts +329 -0
  65. package/src/stories/components/checkbox.stories.ts +102 -0
  66. package/src/stories/components/input.stories.ts +100 -0
  67. package/src/stories/components/search.stories.ts +81 -0
  68. package/src/stories/user.ts +3 -0
  69. package/src/styles.scss +14 -0
  70. package/src/tokens/stories/borders.stories.ts +118 -0
  71. package/src/tokens/stories/colors.stories.ts +90 -0
  72. package/src/tokens/stories/shadows.stories.ts +93 -0
  73. package/src/tokens/stories/spacing.stories.ts +55 -0
  74. package/src/tokens/stories/typography.stories.ts +76 -0
  75. package/src/tokens/styles/_borders.scss +16 -0
  76. package/src/tokens/styles/_colors.scss +76 -0
  77. package/src/tokens/styles/_shadows.scss +9 -0
  78. package/src/tokens/styles/_spacing.scss +17 -0
  79. package/src/tokens/styles/_typography.scss +34 -0
  80. package/src/tokens/styles/tokens.scss +5 -0
  81. package/src/tokens/tokens.ts +42 -0
  82. package/tsconfig.app.json +17 -0
  83. package/tsconfig.json +33 -0
  84. package/tsconfig.spec.json +15 -0
@@ -0,0 +1,84 @@
1
+ import { Component, Input, forwardRef } from '@angular/core';
2
+ import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
3
+ import { LucideAngularModule, Check, Minus } from 'lucide-angular';
4
+
5
+ @Component({
6
+ standalone: true,
7
+ selector: 'ds-checkbox',
8
+ imports: [LucideAngularModule],
9
+ templateUrl: './checkbox.component.html',
10
+ styleUrl: './checkbox.component.scss',
11
+ providers: [
12
+ {
13
+ provide: NG_VALUE_ACCESSOR,
14
+ useExisting: forwardRef(() => CheckboxComponent),
15
+ multi: true,
16
+ },
17
+ ],
18
+ })
19
+ export class CheckboxComponent implements ControlValueAccessor {
20
+ @Input() label = '';
21
+ @Input() hint = '';
22
+ @Input() disabled = false;
23
+
24
+ /** Soporta control externo para "mixed" */
25
+ @Input() indeterminate = false;
26
+
27
+ /** ✅ Para que tus stories funcionen: [checked]="true" */
28
+ @Input()
29
+ get checked(): boolean {
30
+ return this._checked;
31
+ }
32
+ set checked(val: boolean) {
33
+ this._checked = !!val;
34
+ }
35
+
36
+ readonly Check = Check;
37
+ readonly Minus = Minus;
38
+
39
+ private _checked = false;
40
+ isFocused = false;
41
+
42
+ private onChange = (_: boolean) => {};
43
+ private onTouched = () => {};
44
+
45
+ get rootClasses(): string {
46
+ return ['cb-root', this.disabled ? 'cb-root--disabled' : ''].filter(Boolean).join(' ');
47
+ }
48
+
49
+ get boxClasses(): string {
50
+ return [
51
+ 'cb-box',
52
+ (this._checked || this.indeterminate) ? 'cb-box--checked' : '',
53
+ this.disabled ? 'cb-box--disabled' : '',
54
+ this.isFocused ? 'cb-box--focused' : '',
55
+ ].filter(Boolean).join(' ');
56
+ }
57
+
58
+ /** ✅ Un solo punto de toggle */
59
+ toggle(): void {
60
+ if (this.disabled) return;
61
+
62
+ // Si estaba indeterminado, el primer click lo vuelve "checked"
63
+ if (this.indeterminate) {
64
+ this.indeterminate = false;
65
+ this._checked = true;
66
+ } else {
67
+ this._checked = !this._checked;
68
+ }
69
+
70
+ this.onChange(this._checked);
71
+ this.onTouched();
72
+ }
73
+
74
+ onFocus(): void { this.isFocused = true; }
75
+ onBlur(): void { this.isFocused = false; this.onTouched(); }
76
+
77
+ // ControlValueAccessor
78
+ writeValue(val: boolean): void {
79
+ this._checked = !!val;
80
+ }
81
+ registerOnChange(fn: any): void { this.onChange = fn; }
82
+ registerOnTouched(fn: any): void { this.onTouched = fn; }
83
+ setDisabledState(is: boolean): void { this.disabled = is; }
84
+ }
@@ -0,0 +1,43 @@
1
+ <div class="input-field">
2
+
3
+ @if (label) {
4
+ <label class="input-label" [for]="inputId">
5
+ {{ label }}
6
+ @if (required) {
7
+ <span class="input-required" aria-hidden="true">*</span>
8
+ }
9
+ </label>
10
+ }
11
+
12
+ <div [class]="wrapperClasses">
13
+
14
+ @if (prefixIcon) {
15
+ <span class="input-icon input-icon--prefix">
16
+ <lucide-icon [img]="prefixIcon" [size]="16" />
17
+ </span>
18
+ }
19
+
20
+ <input [id]="inputId" [type]="currentType" [placeholder]="placeholder" [disabled]="disabled"
21
+ [required]="required" [value]="value" class="input-control" (input)="onInput($event)" (focus)="onFocus()"
22
+ (blur)="onBlur()" />
23
+
24
+ @if (isPassword) {
25
+ <button type="button" class="input-icon input-icon--suffix input-icon--button" [disabled]="disabled"
26
+ (click)="togglePassword()" [attr.aria-label]="showPassword() ? 'Ocultar contraseña' : 'Mostrar contraseña'">
27
+ <lucide-icon [img]="showPassword() ? EyeOff : Eye" [size]="16" />
28
+ </button>
29
+ } @else if (suffixIcon) {
30
+ <span class="input-icon input-icon--suffix">
31
+ <lucide-icon [img]="suffixIcon" [size]="16" />
32
+ </span>
33
+ }
34
+
35
+ </div>
36
+
37
+ @if (error) {
38
+ <span class="input-hint input-hint--error">{{ error }}</span>
39
+ } @else if (hint) {
40
+ <span class="input-hint">{{ hint }}</span>
41
+ }
42
+
43
+ </div>
@@ -0,0 +1,181 @@
1
+ // ── Field container ───────────────────────────────────────────
2
+ .input-field {
3
+ display: flex;
4
+ flex-direction: column;
5
+ gap: 6px;
6
+ width: 100%;
7
+ }
8
+
9
+ // ── Label ─────────────────────────────────────────────────────
10
+ .input-label {
11
+ font-size: var(--font-size-md);
12
+ font-weight: var(--font-weight-medium);
13
+ color: var(--color-text-primary);
14
+ line-height: 1;
15
+ }
16
+
17
+ // ── Wrapper (el "cuadro" visible) ─────────────────────────────
18
+ .input-wrapper {
19
+ position: relative;
20
+ display: flex;
21
+ align-items: center;
22
+ box-shadow: var(--shadow-xs);
23
+ padding: 7.5px 11px;
24
+ background: var(--color-bg-default);
25
+ border: 1px solid var(--color-border);
26
+ border-radius: var(--radius-lg);
27
+ transition: border-color 150ms ease, box-shadow 150ms ease;
28
+ --icon-slot: 28px;
29
+ --icon-size: 16px;
30
+
31
+ &--focused {
32
+ border-color: var(--color-brand-400);
33
+ box-shadow: var(--shadow-xs);
34
+ border-width: var(--border-width-thin);
35
+ }
36
+
37
+ &--error {
38
+ border-color: var(--color-status-error);
39
+
40
+ &.input-wrapper--focused {
41
+ box-shadow: 0 0 0 3px var(--color-status-error-bg);
42
+ }
43
+ }
44
+
45
+ &--disabled {
46
+ background: var(--color-bg-subtle);
47
+ opacity: 1;
48
+ cursor: not-allowed;
49
+ }
50
+ }
51
+
52
+ // ── Input ───────────────────────────────────────────────────
53
+ .input-control {
54
+ flex: 1;
55
+ border: none;
56
+ outline: none;
57
+ background: transparent;
58
+ overflow: hidden;
59
+ font-family: var(--font-family-sans);
60
+ color: var(--color-text-primary);
61
+ width: 100%;
62
+ padding: 0;
63
+ line-height: 1.2;
64
+ font-size: var(--font-size-md);
65
+
66
+ &::placeholder {
67
+ color: var(--color-text-disabled);
68
+ }
69
+
70
+ &:disabled {
71
+ cursor: not-allowed;
72
+ }
73
+
74
+ &::-ms-reveal,
75
+ &::-ms-clear {
76
+ display: none;
77
+ }
78
+ }
79
+
80
+ .input-wrapper--has-prefix {
81
+ padding-left: calc(2px + var(--icon-slot));
82
+ }
83
+
84
+ .input-wrapper--has-suffix {
85
+ padding-right: calc(11px + var(--icon-slot));
86
+ }
87
+
88
+ .input-wrapper--has-prefix .input-control {
89
+ padding-left: 0;
90
+ }
91
+
92
+ .input-wrapper--has-suffix .input-control {
93
+ padding-right: 0;
94
+ }
95
+
96
+ .input-required {
97
+ margin-left: 4px;
98
+ color: var(--color-status-error);
99
+ font-weight: var(--font-weight-medium);
100
+ line-height: 1;
101
+ }
102
+
103
+ // ── Icons ───────────────────────────────────────────────────
104
+ .input-icon {
105
+ position: absolute;
106
+ top: 50%;
107
+ transform: translateY(-50%);
108
+
109
+ display: inline-flex;
110
+ align-items: center;
111
+ justify-content: center;
112
+
113
+ color: var(--color-text-tertiary);
114
+
115
+ width: var(--icon-slot);
116
+ height: var(--icon-slot);
117
+ line-height: 0;
118
+
119
+ &--prefix {
120
+ left: 0;
121
+ }
122
+
123
+ &--suffix {
124
+ right: 1px;
125
+ }
126
+ }
127
+
128
+ .input-icon lucide-icon,
129
+ .input-icon svg {
130
+ width: var(--icon-size);
131
+ height: var(--icon-size);
132
+ display: block;
133
+ }
134
+
135
+ /* Suffix interactivo (password toggle) */
136
+ .input-icon--button {
137
+ background: transparent;
138
+ border: 0;
139
+ padding: 0;
140
+
141
+ /* Hit area consistente */
142
+ width: var(--icon-slot);
143
+ height: var(--icon-slot);
144
+
145
+ display: inline-flex;
146
+ align-items: center;
147
+ justify-content: center;
148
+
149
+ color: var(--color-text-tertiary);
150
+ cursor: pointer;
151
+ border-radius: var(--radius-md);
152
+ line-height: 0;
153
+
154
+ &:hover {
155
+ color: var(--color-text-primary);
156
+ background: var(--color-bg-subtle);
157
+ }
158
+
159
+ &:focus-visible {
160
+ outline: none;
161
+ box-shadow: 0 0 0 3px var(--color-brand-200);
162
+ }
163
+
164
+ &:disabled {
165
+ cursor: not-allowed;
166
+ opacity: 0.6;
167
+ }
168
+ }
169
+
170
+ // ── Hint / Error ──────────────────────────────────────────────
171
+ .input-hint {
172
+ font-family: var(--font-family-sans);
173
+ font-size: var(--font-size-md);
174
+ font-weight: var(--font-weight-regular);
175
+ color: var(--color-text-secondary);
176
+ line-height: 1;
177
+
178
+ &--error {
179
+ color: var(--color-status-error-text);
180
+ }
181
+ }
@@ -0,0 +1,87 @@
1
+ import { Component, Input, forwardRef, signal } from '@angular/core';
2
+ import { ControlValueAccessor, NG_VALUE_ACCESSOR, ReactiveFormsModule } from '@angular/forms';
3
+ import { LucideAngularModule, Eye, EyeOff } from 'lucide-angular';
4
+
5
+ @Component({
6
+ standalone: true,
7
+ selector: 'ds-input',
8
+ imports: [LucideAngularModule, ReactiveFormsModule],
9
+ templateUrl: './input.component.html',
10
+ styleUrl: './input.component.scss',
11
+ providers: [
12
+ {
13
+ provide: NG_VALUE_ACCESSOR,
14
+ useExisting: forwardRef(() => InputComponent),
15
+ multi: true,
16
+ },
17
+ ],
18
+ })
19
+ export class InputComponent implements ControlValueAccessor {
20
+ @Input() label = '';
21
+ @Input() placeholder = '';
22
+ @Input() hint = '';
23
+ @Input() error = '';
24
+ @Input() type = 'text';
25
+ @Input() disabled = false;
26
+ @Input() prefixIcon: any = null;
27
+ @Input() suffixIcon: any = null;
28
+ @Input() required = false;
29
+
30
+ readonly Eye = Eye;
31
+ readonly EyeOff = EyeOff;
32
+
33
+ value = '';
34
+ isFocused = false;
35
+ showPassword = signal(false);
36
+
37
+ private onChange = (_: any) => { };
38
+ private onTouched = () => { };
39
+
40
+ private static nextId = 0;
41
+ readonly inputId = `ds-input-${InputComponent.nextId++}`;
42
+
43
+ get currentType(): string {
44
+ if (this.type === 'password') {
45
+ return this.showPassword() ? 'text' : 'password';
46
+ }
47
+ return this.type;
48
+ }
49
+
50
+ get isPassword(): boolean {
51
+ return this.type === 'password';
52
+ }
53
+
54
+ get hasError(): boolean {
55
+ return !!this.error;
56
+ }
57
+
58
+ get wrapperClasses(): string {
59
+ return [
60
+ 'input-wrapper',
61
+ this.isFocused ? 'input-wrapper--focused' : '',
62
+ this.hasError ? 'input-wrapper--error' : '',
63
+ this.disabled ? 'input-wrapper--disabled' : '',
64
+ this.prefixIcon ? 'input-wrapper--has-prefix' : '',
65
+ (this.suffixIcon || this.isPassword) ? 'input-wrapper--has-suffix' : '',
66
+ ].filter(Boolean).join(' ');
67
+ }
68
+
69
+ togglePassword(): void {
70
+ this.showPassword.update(v => !v);
71
+ }
72
+
73
+ onInput(event: Event): void {
74
+ const val = (event.target as HTMLInputElement).value;
75
+ this.value = val;
76
+ this.onChange(val);
77
+ }
78
+
79
+ onFocus(): void { this.isFocused = true; }
80
+ onBlur(): void { this.isFocused = false; this.onTouched(); }
81
+
82
+ // ControlValueAccessor
83
+ writeValue(val: string): void { this.value = val ?? ''; }
84
+ registerOnChange(fn: any): void { this.onChange = fn; }
85
+ registerOnTouched(fn: any): void { this.onTouched = fn; }
86
+ setDisabledState(is: boolean): void { this.disabled = is; }
87
+ }
@@ -0,0 +1,30 @@
1
+ <div [class]="wrapperClasses">
2
+
3
+ <span class="search-icon search-icon--prefix">
4
+ <lucide-icon [img]="Search" [size]="iconSize" />
5
+ </span>
6
+
7
+ <input
8
+ class="search-control"
9
+ type="text"
10
+ [placeholder]="placeholder"
11
+ [disabled]="disabled"
12
+ [value]="value"
13
+ (input)="onInput($event)"
14
+ (keydown)="onKeydown($event)"
15
+ (focus)="onFocus()"
16
+ (blur)="onBlur()"
17
+ />
18
+
19
+ @if (value && !disabled) {
20
+ <button
21
+ type="button"
22
+ class="search-icon search-icon--clear"
23
+ (click)="clear()"
24
+ aria-label="Limpiar búsqueda"
25
+ >
26
+ <lucide-icon [img]="X" [size]="iconSize" />
27
+ </button>
28
+ }
29
+
30
+ </div>
@@ -0,0 +1,102 @@
1
+ // ── Wrapper ───────────────────────────────────────────────────
2
+ .search-wrapper {
3
+ --icon-slot: 20px;
4
+ --icon-size: 16px;
5
+
6
+ position: relative;
7
+ display: flex;
8
+ align-items: center;
9
+ width: 100%;
10
+ padding: 8px 6px;
11
+ background: var(--search-bg);
12
+ border: 1.5px solid transparent;
13
+ border-radius: var(--radius-lg);
14
+ transition: background 150ms ease, border-color 150ms ease, box-shadow 150ms ease;
15
+
16
+ &--focused {
17
+ background: var(--search-bg);
18
+ border-color: var(--color-brand-400);
19
+ box-shadow: 0 0 0 3px var(--color-brand-100);
20
+ }
21
+
22
+ &--disabled {
23
+ opacity: 0.5;
24
+ cursor: not-allowed;
25
+ }
26
+ }
27
+
28
+ // ── Input ──────────────────────────────────────────────
29
+ .search-control {
30
+ flex: 1;
31
+ height: 100%;
32
+ border: none;
33
+ outline: none;
34
+ background: transparent;
35
+ font-family: var(--font-family-sans);
36
+ font-size: var(--font-size-md);
37
+ color: var(--color-text-primary);
38
+ padding-inline: 0;
39
+
40
+ &::placeholder {
41
+ color: var(--color-text-tertiary);
42
+ }
43
+
44
+ &:disabled {
45
+ cursor: not-allowed;
46
+ }
47
+ }
48
+
49
+ // ── Icons ────────────────────────────────────────────────────
50
+ .search-icon {
51
+ width: var(--icon-slot);
52
+ height: var(--icon-slot);
53
+
54
+ display: inline-flex;
55
+ align-items: center;
56
+ justify-content: center;
57
+
58
+ flex: 0 0 auto;
59
+ line-height: 0;
60
+ color: var(--color-text-disabled);
61
+ }
62
+
63
+ .search-icon--prefix {
64
+ margin-left: 6px;
65
+ margin-right: 6px;
66
+ }
67
+
68
+ .search-icon--clear {
69
+ width: var(--icon-slot);
70
+ height: var(--icon-slot);
71
+
72
+ background: transparent;
73
+ border: 0;
74
+ padding: 0;
75
+
76
+ display: inline-flex;
77
+ align-items: center;
78
+ justify-content: center;
79
+
80
+ cursor: pointer;
81
+ color: var(--color-text-tertiary);
82
+ border-radius: 999px;
83
+ line-height: 0;
84
+ transition: color 150ms ease, background 150ms ease;
85
+
86
+ &:hover {
87
+ color: var(--color-text-primary);
88
+ background: var(--color-bg-subtle);
89
+ }
90
+
91
+ &:focus-visible {
92
+ outline: none;
93
+ box-shadow: 0 0 0 3px var(--color-brand-100);
94
+ }
95
+ }
96
+
97
+ .search-icon lucide-icon,
98
+ .search-icon svg {
99
+ width: var(--icon-size);
100
+ height: var(--icon-size);
101
+ display: block;
102
+ }
@@ -0,0 +1,73 @@
1
+ import { Component, Input, Output, EventEmitter, forwardRef } from '@angular/core';
2
+ import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
3
+ import { LucideAngularModule, Search, X } from 'lucide-angular';
4
+
5
+ @Component({
6
+ standalone: true,
7
+ selector: 'ds-search',
8
+ imports: [LucideAngularModule],
9
+ templateUrl: './search.component.html',
10
+ styleUrl: './search.component.scss',
11
+ providers: [
12
+ {
13
+ provide: NG_VALUE_ACCESSOR,
14
+ useExisting: forwardRef(() => SearchComponent),
15
+ multi: true,
16
+ },
17
+ ],
18
+ })
19
+ export class SearchComponent implements ControlValueAccessor {
20
+ @Input() placeholder = 'Buscar...';
21
+ @Input() disabled = false;
22
+
23
+ @Output() search = new EventEmitter<string>();
24
+ @Output() cleared = new EventEmitter<void>();
25
+
26
+ readonly Search = Search;
27
+ readonly X = X;
28
+
29
+ value = '';
30
+ isFocused = false;
31
+
32
+ private onChange = (_: any) => {};
33
+ private onTouched = () => {};
34
+
35
+ get wrapperClasses(): string {
36
+ return [
37
+ 'search-wrapper',
38
+ this.isFocused ? 'search-wrapper--focused' : '',
39
+ this.disabled ? 'search-wrapper--disabled' : '',
40
+ ].filter(Boolean).join(' ');
41
+ }
42
+
43
+ get iconSize(): number {
44
+ return 16;
45
+ }
46
+
47
+ onInput(event: Event): void {
48
+ const val = (event.target as HTMLInputElement).value;
49
+ this.value = val;
50
+ this.onChange(val);
51
+ this.search.emit(val);
52
+ }
53
+
54
+ onKeydown(event: KeyboardEvent): void {
55
+ if (event.key === 'Enter') this.search.emit(this.value);
56
+ if (event.key === 'Escape') this.clear();
57
+ }
58
+
59
+ clear(): void {
60
+ this.value = '';
61
+ this.onChange('');
62
+ this.cleared.emit();
63
+ this.search.emit('');
64
+ }
65
+
66
+ onFocus(): void { this.isFocused = true; }
67
+ onBlur(): void { this.isFocused = false; this.onTouched(); }
68
+
69
+ writeValue(val: string): void { this.value = val ?? ''; }
70
+ registerOnChange(fn: any): void { this.onChange = fn; }
71
+ registerOnTouched(fn: any): void { this.onTouched = fn; }
72
+ setDisabledState(is: boolean): void { this.disabled = is; }
73
+ }
package/src/index.html ADDED
@@ -0,0 +1,13 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>GobDs</title>
6
+ <base href="/">
7
+ <meta name="viewport" content="width=device-width, initial-scale=1">
8
+ <link rel="icon" type="image/x-icon" href="favicon.ico">
9
+ </head>
10
+ <body>
11
+ <app-root></app-root>
12
+ </body>
13
+ </html>
@@ -0,0 +1,8 @@
1
+ import { BootstrapContext, bootstrapApplication } from '@angular/platform-browser';
2
+ import { App } from './app/app';
3
+ import { config } from './app/app.config.server';
4
+
5
+ const bootstrap = (context: BootstrapContext) =>
6
+ bootstrapApplication(App, config, context);
7
+
8
+ export default bootstrap;
package/src/main.ts ADDED
@@ -0,0 +1,6 @@
1
+ import { bootstrapApplication } from '@angular/platform-browser';
2
+ import { appConfig } from './app/app.config';
3
+ import { App } from './app/app';
4
+
5
+ bootstrapApplication(App, appConfig)
6
+ .catch((err) => console.error(err));
package/src/server.ts ADDED
@@ -0,0 +1,68 @@
1
+ import {
2
+ AngularNodeAppEngine,
3
+ createNodeRequestHandler,
4
+ isMainModule,
5
+ writeResponseToNodeResponse,
6
+ } from '@angular/ssr/node';
7
+ import express from 'express';
8
+ import { join } from 'node:path';
9
+
10
+ const browserDistFolder = join(import.meta.dirname, '../browser');
11
+
12
+ const app = express();
13
+ const angularApp = new AngularNodeAppEngine();
14
+
15
+ /**
16
+ * Example Express Rest API endpoints can be defined here.
17
+ * Uncomment and define endpoints as necessary.
18
+ *
19
+ * Example:
20
+ * ```ts
21
+ * app.get('/api/{*splat}', (req, res) => {
22
+ * // Handle API request
23
+ * });
24
+ * ```
25
+ */
26
+
27
+ /**
28
+ * Serve static files from /browser
29
+ */
30
+ app.use(
31
+ express.static(browserDistFolder, {
32
+ maxAge: '1y',
33
+ index: false,
34
+ redirect: false,
35
+ }),
36
+ );
37
+
38
+ /**
39
+ * Handle all other requests by rendering the Angular application.
40
+ */
41
+ app.use((req, res, next) => {
42
+ angularApp
43
+ .handle(req)
44
+ .then((response) =>
45
+ response ? writeResponseToNodeResponse(response, res) : next(),
46
+ )
47
+ .catch(next);
48
+ });
49
+
50
+ /**
51
+ * Start the server if this module is the main entry point, or it is ran via PM2.
52
+ * The server listens on the port defined by the `PORT` environment variable, or defaults to 4000.
53
+ */
54
+ if (isMainModule(import.meta.url) || process.env['pm_id']) {
55
+ const port = process.env['PORT'] || 4000;
56
+ app.listen(port, (error) => {
57
+ if (error) {
58
+ throw error;
59
+ }
60
+
61
+ console.log(`Node Express server listening on http://localhost:${port}`);
62
+ });
63
+ }
64
+
65
+ /**
66
+ * Request handler used by the Angular CLI (for dev-server and during build) or Firebase Cloud Functions.
67
+ */
68
+ export const reqHandler = createNodeRequestHandler(app);