@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,144 @@
1
+ import {
2
+ Component,
3
+ ElementRef,
4
+ EventEmitter,
5
+ HostListener,
6
+ Input,
7
+ OnChanges,
8
+ OnDestroy,
9
+ Output,
10
+ SimpleChanges,
11
+ ViewChild,
12
+ } from '@angular/core';
13
+ import { CommonModule } from '@angular/common';
14
+
15
+ export type AlertDialogVariant = 'default' | 'destructive';
16
+
17
+ @Component({
18
+ standalone: true,
19
+ selector: 'ds-alert-dialog',
20
+ imports: [CommonModule],
21
+ templateUrl: './alert-dialog.component.html',
22
+ styleUrl: './alert-dialog.component.scss',
23
+ })
24
+ export class AlertDialogComponent implements OnChanges, OnDestroy {
25
+ @Input() open = false;
26
+ @Output() openChange = new EventEmitter<boolean>();
27
+
28
+ @Input() title = 'Confirmar acción';
29
+ @Input() description = '';
30
+ @Input() variant: AlertDialogVariant = 'default';
31
+
32
+ @Input() confirmText = 'Confirmar';
33
+ @Input() cancelText = 'Cancelar';
34
+
35
+ @Input() closeOnOverlayClick = true;
36
+ @Input() disableClose = false;
37
+
38
+ @Output() confirm = new EventEmitter<void>();
39
+ @Output() cancel = new EventEmitter<void>();
40
+
41
+ @ViewChild('dialogPanel', { static: false }) dialogPanel?: ElementRef<HTMLElement>;
42
+ @ViewChild('confirmBtn', { static: false }) confirmBtn?: ElementRef<HTMLButtonElement>;
43
+
44
+ private lastActiveElement: HTMLElement | null = null;
45
+
46
+ ngOnChanges(changes: SimpleChanges): void {
47
+ if (changes['open']) {
48
+ if (this.open) this.onOpen();
49
+ else this.onClose();
50
+ }
51
+ }
52
+
53
+ ngOnDestroy(): void {
54
+ this.unlockScroll();
55
+ }
56
+
57
+ private onOpen(): void {
58
+ this.lastActiveElement = document.activeElement as HTMLElement | null;
59
+ this.lockScroll();
60
+
61
+ queueMicrotask(() => {
62
+ this.confirmBtn?.nativeElement?.focus();
63
+ });
64
+ }
65
+
66
+ private onClose(): void {
67
+ this.unlockScroll();
68
+ queueMicrotask(() => {
69
+ this.lastActiveElement?.focus?.();
70
+ });
71
+ }
72
+
73
+ private lockScroll(): void {
74
+ document.documentElement.style.overflow = 'hidden';
75
+ }
76
+
77
+ private unlockScroll(): void {
78
+ document.documentElement.style.overflow = '';
79
+ }
80
+
81
+ requestClose(): void {
82
+ if (this.disableClose) return;
83
+ this.openChange.emit(false);
84
+ }
85
+
86
+ onOverlayClick(): void {
87
+ if (!this.closeOnOverlayClick) return;
88
+ this.requestClose();
89
+ }
90
+
91
+ onCancel(): void {
92
+ this.cancel.emit();
93
+ this.requestClose();
94
+ }
95
+
96
+ onConfirm(): void {
97
+ this.confirm.emit();
98
+ this.requestClose();
99
+ }
100
+
101
+ @HostListener('document:keydown', ['$event'])
102
+ onKeydown(event: KeyboardEvent): void {
103
+ if (!this.open) return;
104
+
105
+ if (event.key === 'Escape') {
106
+ event.preventDefault();
107
+ this.requestClose();
108
+ return;
109
+ }
110
+
111
+ if (event.key === 'Tab') {
112
+ this.trapFocus(event);
113
+ }
114
+ }
115
+
116
+ private trapFocus(event: KeyboardEvent): void {
117
+ const panel = this.dialogPanel?.nativeElement;
118
+ if (!panel) return;
119
+
120
+ const focusables = Array.from(
121
+ panel.querySelectorAll<HTMLElement>(
122
+ 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
123
+ )
124
+ ).filter(el => !el.hasAttribute('disabled') && !el.getAttribute('aria-disabled'));
125
+
126
+ if (focusables.length === 0) return;
127
+
128
+ const first = focusables[0];
129
+ const last = focusables[focusables.length - 1];
130
+ const active = document.activeElement as HTMLElement | null;
131
+
132
+ if (event.shiftKey) {
133
+ if (!active || active === first) {
134
+ event.preventDefault();
135
+ last.focus();
136
+ }
137
+ } else {
138
+ if (!active || active === last) {
139
+ event.preventDefault();
140
+ first.focus();
141
+ }
142
+ }
143
+ }
144
+ }
@@ -0,0 +1,25 @@
1
+ import { Component, Input } from '@angular/core';
2
+ import { CommonModule } from '@angular/common';
3
+
4
+ export type BadgeVariant = 'success' | 'warning' | 'neutral' | 'default' | 'error';
5
+ export type BadgeSize = 'sm' | 'md';
6
+
7
+ @Component({
8
+ standalone: true,
9
+ selector: 'cg-badge',
10
+ imports: [CommonModule],
11
+ styleUrl: './badge.scss',
12
+ template: `
13
+ <span [class]="badgeClasses">
14
+ <ng-content />
15
+ </span>
16
+ `,
17
+ })
18
+ export class BadgeComponent {
19
+ @Input() variant: BadgeVariant = 'default';
20
+ @Input() size: BadgeSize = 'md';
21
+
22
+ get badgeClasses(): string {
23
+ return `badge badge--${this.variant} badge--${this.size}`;
24
+ }
25
+ }
@@ -0,0 +1,50 @@
1
+ .badge {
2
+ font-weight: var(--font-weight-medium);
3
+ border-radius: var(--radius-md);
4
+ white-space: nowrap;
5
+ width: fit-content;
6
+ padding: var(--space-0-5) var(--space-1-5);
7
+ }
8
+
9
+ /* Sizes */
10
+
11
+ .badge--sm {
12
+ font-size: var(--font-size-sm);
13
+ }
14
+
15
+ .badge--md {
16
+ font-size: var(--font-size-md);
17
+ }
18
+
19
+ /* Variants */
20
+
21
+ .badge--success {
22
+ background: var(--color-status-success-bg);
23
+ color: var(--color-status-success-text);
24
+ border: 1px solid var(--color-status-success-border);
25
+ }
26
+
27
+ .badge--warning {
28
+ background: var(--color-status-warning-bg);
29
+ color: var(--color-status-warning-text);
30
+ border: 1px solid var(--color-status-warning-border);
31
+ }
32
+
33
+ .badge--neutral {
34
+ background: var(--color-status-neutral-bg);
35
+ color: var(--color-status-neutral-text);
36
+ border: 1px solid var(--color-status-neutral-border);
37
+ }
38
+
39
+ .badge--default {
40
+ background: var(--color-status-default-bg);
41
+ color: var(--color-status-default-text);
42
+ border: 1px solid var(--color-status-default-border);
43
+ }
44
+
45
+ .badge--error {
46
+ background: var(--color-status-error-bg);
47
+ color: var(--color-status-error-text);
48
+ border: 1px solid var(--color-status-error-border);
49
+ }
50
+
@@ -0,0 +1,35 @@
1
+ <ng-template #content>
2
+ @if (loading) {
3
+ <lucide-icon [img]="Loader2" [size]="iconSize" class="btn__spinner" />
4
+ }
5
+
6
+ @if (showIconLeft) {
7
+ <lucide-icon [img]="icon" [size]="iconSize" class="btn__icon" />
8
+ }
9
+
10
+ @if (iconPosition !== 'only') {
11
+ <span class="btn__label">
12
+ <ng-content />
13
+ </span>
14
+ }
15
+
16
+ @if (showIconOnly) {
17
+ <lucide-icon [img]="icon" [size]="iconSize" class="btn__icon" />
18
+ }
19
+
20
+ @if (showIconRight) {
21
+ <lucide-icon [img]="icon" [size]="iconSize" class="btn__icon" />
22
+ }
23
+ </ng-template>
24
+
25
+ @if (asTag === 'a') {
26
+ <a [class]="hostClasses + ' ' + stateClasses" [attr.aria-disabled]="isDisabled">
27
+ <ng-container *ngTemplateOutlet="content" />
28
+ </a>
29
+ } @else {
30
+ <button [class]="hostClasses + ' ' + stateClasses"
31
+ [disabled]="isDisabled"
32
+ [attr.aria-busy]="loading">
33
+ <ng-container *ngTemplateOutlet="content" />
34
+ </button>
35
+ }
@@ -0,0 +1,226 @@
1
+ // ── Button Tokens ───────────────────────────────────────────
2
+ :host {
3
+ display: inline-flex;
4
+ }
5
+
6
+ // ── Base ──────────────────────────────────────────────────────
7
+ .btn {
8
+ display: inline-flex;
9
+ align-items: center;
10
+ justify-content: center;
11
+ gap: 8px;
12
+ font-family: var(--font-family-sans);
13
+ font-weight: var(--font-weight-regular);
14
+ border: var(--border-width-thin) solid transparent;
15
+ border-radius: var(--btn-radius);
16
+ cursor: pointer;
17
+ white-space: nowrap;
18
+ text-decoration: none;
19
+ transition: background 150ms ease, color 150ms ease,
20
+ border-color 150ms ease, box-shadow 150ms ease,
21
+ opacity 150ms ease;
22
+ outline: none;
23
+
24
+ &:focus-visible {
25
+ box-shadow: 0 0 0 3px var(--color-brand-200);
26
+ }
27
+ }
28
+
29
+ // ── Sizes ───────────────────────────────────────────────────
30
+ .btn--sm {
31
+ --btn-radius: var(--radius-lg);
32
+ height: auto;
33
+ padding: 6px 10px;
34
+ font-size: var(--font-size-sm);
35
+
36
+ &.btn--icon-only {
37
+ width: 28px;
38
+ padding: 0;
39
+ height: 28px;
40
+ }
41
+ }
42
+
43
+ .btn--md {
44
+ --btn-radius: var(--radius-lg);
45
+ height: auto;
46
+ padding: 6px 12px;
47
+ font-size: var(--font-size-md);
48
+
49
+ &.btn--icon-only {
50
+ width: 36px;
51
+ padding: 0;
52
+ height: 36px;
53
+ }
54
+ }
55
+
56
+ .btn--lg {
57
+ --btn-radius: var(--radius-lg);
58
+ height: auto;
59
+ padding: 8px 16px;
60
+ font-size: var(--font-size-lg);
61
+
62
+ &.btn--icon-only {
63
+ width: 44px;
64
+ padding: 0;
65
+ height: 44px;
66
+ }
67
+ }
68
+
69
+ // ── Full width ────────────────────────────────────────────────
70
+ .btn--full {
71
+ width: 100%;
72
+ }
73
+
74
+ // ── Variants ─────────────────────────────────────────────────
75
+
76
+ // Primary
77
+ .btn--primary {
78
+ background: var(--color-brand-700);
79
+ color: #fff;
80
+
81
+ &:is(:hover, .is-hover):not(.btn--disabled) {
82
+ background: var(--color-brand-800);
83
+ }
84
+
85
+ &:is(:active, .is-active):not(.btn--disabled) {
86
+ background: var(--color-brand-900);
87
+ }
88
+ }
89
+
90
+ // Secondary
91
+ .btn--secondary {
92
+ background: var(--color-neutral-300);
93
+ color: var(--color-text-primary);
94
+
95
+ &:is(:hover, .is-hover):not(.btn--disabled) {
96
+ background: var(--color-neutral-300);
97
+ }
98
+
99
+ &:is(:active, .is-active):not(.btn--disabled) {
100
+ background: var(--color-neutral-500);
101
+ }
102
+
103
+ // o define neutral-400
104
+ }
105
+
106
+ // Outline
107
+ .btn--outline {
108
+ background: transparent;
109
+ color: var(--color-text-primary);
110
+ border-color: var(--color-border);
111
+ border-width: var(--border-width-thin);
112
+ box-shadow: var(--shadow-xs);
113
+
114
+ &:is(:hover, .is-hover):not(.btn--disabled) {
115
+ background: var(--color-bg-subtle);
116
+ border-color: var(--color-border-strong);
117
+ }
118
+
119
+ &:is(:active, .is-active):not(.btn--disabled) {
120
+ background: var(--color-bg-muted);
121
+ }
122
+ }
123
+
124
+ // Ghost
125
+ .btn--ghost {
126
+ background: transparent;
127
+ color: var(--color-text-primary);
128
+ border-color: transparent;
129
+
130
+ &:hover:not(.btn--disabled) {
131
+ background: var(--color-bg-muted);
132
+ }
133
+
134
+ &:active:not(.btn--disabled) {
135
+ background: var(--color-gray-200);
136
+ }
137
+ }
138
+
139
+ // Destructive
140
+ .btn--destructive {
141
+ background: #EF4444;
142
+ color: #fff;
143
+
144
+ &:is(:hover, .is-hover):not(.btn--disabled) {
145
+ background: #DC2626;
146
+ }
147
+
148
+ &:is(:active, .is-active):not(.btn--disabled) {
149
+ background: #B91C1C;
150
+ }
151
+
152
+ &:is(:focus-visible, .is-focus) {
153
+ box-shadow: 0 0 0 3px #FECACA;
154
+ }
155
+ }
156
+
157
+ // Link
158
+ .btn--link {
159
+ background: transparent;
160
+ color: var(--color-brand-700);
161
+ padding-inline: 2px;
162
+ height: auto;
163
+
164
+ &:is(:hover, .is-hover):not(.btn--disabled) {
165
+ text-decoration: underline;
166
+ text-underline-offset: 3px;
167
+ }
168
+ }
169
+
170
+ // ── Disabled / loading state ─────────────────────────────────
171
+ .btn--disabled {
172
+ opacity: 0.5;
173
+ cursor: not-allowed;
174
+ pointer-events: none;
175
+ }
176
+
177
+ .btn {
178
+ &:is(:focus-visible, .is-focus) {
179
+ box-shadow: 0 0 0 3px var(--color-brand-200);
180
+ }
181
+ }
182
+
183
+ // ── Spinner ───────────────────────────────────────────────────
184
+ .btn__spinner {
185
+ animation: spin 1.2s linear infinite;
186
+ flex-shrink: 0;
187
+ }
188
+
189
+ @keyframes spin {
190
+ from {
191
+ transform: rotate(0deg);
192
+ }
193
+
194
+ to {
195
+ transform: rotate(360deg);
196
+ }
197
+ }
198
+
199
+ // ── Icon ───────────────────────────────────────────────────────
200
+ .btn__icon,
201
+ .btn__spinner {
202
+ display: inline-flex;
203
+ align-items: center;
204
+ justify-content: center;
205
+ flex: 0 0 auto;
206
+ line-height: 0;
207
+ }
208
+
209
+ .btn__icon svg,
210
+ .btn__spinner svg {
211
+ display: block;
212
+ width: 1em;
213
+ height: 1em;
214
+ }
215
+
216
+ .btn--sm {
217
+ --btn-icon-size: 14px;
218
+ }
219
+
220
+ .btn--md {
221
+ --btn-icon-size: 16px;
222
+ }
223
+
224
+ .btn--lg {
225
+ --btn-icon-size: 18px;
226
+ }
@@ -0,0 +1,70 @@
1
+ import { Component, ElementRef, Input, inject } from '@angular/core';
2
+ import { CommonModule } from '@angular/common';
3
+ import { LucideAngularModule, Loader2 } from 'lucide-angular';
4
+
5
+ export type ButtonVariant = 'primary' | 'secondary' | 'outline' | 'ghost' | 'destructive' | 'link';
6
+ export type ButtonSize = 'sm' | 'md' | 'lg';
7
+ export type ButtonIconPosition = 'left' | 'right' | 'only';
8
+ export type ButtonAs = 'button' | 'a';
9
+
10
+ @Component({
11
+ standalone: true,
12
+ selector: 'ds-button',
13
+ imports: [CommonModule, LucideAngularModule],
14
+ templateUrl: './button.component.html',
15
+ styleUrl: './button.component.scss',
16
+ })
17
+ export class ButtonComponent {
18
+ private readonly hostEl = inject(ElementRef<HTMLElement>);
19
+
20
+ @Input() variant: ButtonVariant = 'primary';
21
+ @Input() size: ButtonSize = 'md';
22
+ @Input() disabled = false;
23
+ @Input() loading = false;
24
+ @Input() fullWidth = false;
25
+ @Input() iconPosition: ButtonIconPosition | null = null;
26
+ @Input() asTag: ButtonAs = 'button';
27
+ @Input() icon: any = null;
28
+
29
+ readonly Loader2 = Loader2;
30
+
31
+ get hostClasses(): string {
32
+ const classes = [
33
+ 'btn',
34
+ `btn--${this.variant}`,
35
+ `btn--${this.size}`,
36
+ this.fullWidth ? 'btn--full' : '',
37
+ this.iconPosition === 'only' ? 'btn--icon-only' : '',
38
+ this.loading ? 'btn--loading' : '',
39
+ this.disabled || this.loading ? 'btn--disabled' : '',
40
+ ];
41
+ return classes.filter(Boolean).join(' ');
42
+ }
43
+
44
+ // ✅ Toma clases del host (Storybook) y pásalas al <button>/<a>
45
+ get stateClasses(): string {
46
+ const host = this.hostEl.nativeElement;
47
+ const allowed = ['is-hover', 'is-active', 'is-focus'];
48
+ return allowed.filter((c) => host.classList.contains(c)).join(' ');
49
+ }
50
+
51
+ get isDisabled(): boolean {
52
+ return this.disabled || this.loading;
53
+ }
54
+
55
+ get showIconLeft(): boolean {
56
+ return !!this.icon && this.iconPosition === 'left' && !this.loading;
57
+ }
58
+
59
+ get showIconRight(): boolean {
60
+ return !!this.icon && this.iconPosition === 'right' && !this.loading;
61
+ }
62
+
63
+ get showIconOnly(): boolean {
64
+ return !!this.icon && this.iconPosition === 'only' && !this.loading;
65
+ }
66
+
67
+ get iconSize(): number {
68
+ return { sm: 14, md: 16, lg: 18 }[this.size];
69
+ }
70
+ }
@@ -0,0 +1,34 @@
1
+ <label
2
+ [class]="rootClasses"
3
+ [attr.aria-disabled]="disabled"
4
+ >
5
+ <!-- Box accesible -->
6
+ <span
7
+ [class]="boxClasses"
8
+ role="checkbox"
9
+ tabindex="0"
10
+ [attr.aria-checked]="indeterminate ? 'mixed' : checked"
11
+ [attr.aria-disabled]="disabled"
12
+ (click)="toggle()"
13
+ (keydown.space)="$event.preventDefault(); toggle()"
14
+ (keydown.enter)="$event.preventDefault(); toggle()"
15
+ (focus)="onFocus()"
16
+ (blur)="onBlur()"
17
+ >
18
+ @if (indeterminate) {
19
+ <lucide-icon [img]="Minus" class="cb-icon" />
20
+ } @else if (checked) {
21
+ <lucide-icon [img]="Check" class="cb-icon" />
22
+ }
23
+ </span>
24
+
25
+ <!-- Label + hint (click también toggleará) -->
26
+ @if (label) {
27
+ <span class="cb-content" (click)="toggle()">
28
+ <span class="cb-label">{{ label }}</span>
29
+ @if (hint) {
30
+ <span class="cb-hint">{{ hint }}</span>
31
+ }
32
+ </span>
33
+ }
34
+ </label>
@@ -0,0 +1,80 @@
1
+ // ── Root ──────────────────────────────────────────────────────
2
+ .cb-root {
3
+ display: inline-flex;
4
+ align-items: flex-start;
5
+ gap: 10px;
6
+ cursor: pointer;
7
+ user-select: none;
8
+
9
+ &--disabled {
10
+ opacity: 0.5;
11
+ cursor: not-allowed;
12
+ }
13
+ }
14
+
15
+ // ── Box visual ────────────────────────────────────────────────
16
+ .cb-box {
17
+ flex-shrink: 0;
18
+ display: inline-flex;
19
+ align-items: center;
20
+ justify-content: center;
21
+ border-radius: var(--radius-md);
22
+ border: 1.5px solid var(--color-border);
23
+ background: var(--color-bg-default);
24
+ transition: background 150ms ease,
25
+ border-color 150ms ease,
26
+ box-shadow 150ms ease;
27
+ width: 20px;
28
+ height: 20px;
29
+
30
+ // Checked / indeterminate
31
+ &--checked {
32
+ background: var(--color-brand-700);
33
+ border-color: var(--color-brand-700);
34
+ }
35
+
36
+ // Focus ring
37
+ &--focused {
38
+ box-shadow: 0 0 0 3px var(--color-brand-100);
39
+ }
40
+
41
+ &--disabled { cursor: not-allowed; }
42
+ }
43
+
44
+ // ── Ícono interno ─────────────────────────────────────────────
45
+ .cb-icon {
46
+ color: var(--color-white);
47
+ flex-shrink: 0;
48
+ stroke-width: 3px;
49
+ width: 12px;
50
+ height: 12px;
51
+ display: flex;
52
+ align-items: center;
53
+ justify-content: center;
54
+ line-height: 0;
55
+ }
56
+
57
+ .cb-icon svg {
58
+ display: block;
59
+ width: 100%;
60
+ height: 100%;
61
+ }
62
+
63
+ // ── Label + hint ──────────────────────────────────────────────
64
+ .cb-content {
65
+ display: flex;
66
+ flex-direction: column;
67
+ gap: 2px;
68
+ }
69
+
70
+ .cb-label {
71
+ font-size: var(--font-size-md);
72
+ font-weight: var(--font-weight-medium);
73
+ color: var(--color-text-primary);
74
+ }
75
+
76
+ .cb-hint {
77
+ font-size: var(--font-size-md);
78
+ font-weight: var(--font-weight-regular);
79
+ color: var(--color-text-tertiary);
80
+ }