ngx-opalbytes-components 1.19.0 → 1.21.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 (95) hide show
  1. package/README.md +103 -0
  2. package/cli/cao-comp.cjs +456 -0
  3. package/fesm2022/ngx-opalbytes-components.mjs +23 -7
  4. package/fesm2022/ngx-opalbytes-components.mjs.map +1 -1
  5. package/package.json +4 -1
  6. package/src/lib/shared/components/autocomplete/autocomplete.component.html +126 -0
  7. package/src/lib/shared/components/autocomplete/autocomplete.component.scss +150 -0
  8. package/src/lib/shared/components/autocomplete/autocomplete.component.ts +552 -0
  9. package/src/lib/shared/components/autocomplete/autocomplete.stories.ts +133 -0
  10. package/src/lib/shared/components/base-button/base-button.component.html +33 -0
  11. package/src/lib/shared/components/base-button/base-button.component.scss +14 -0
  12. package/src/lib/shared/components/base-button/base-button.component.spec.ts +64 -0
  13. package/src/lib/shared/components/base-button/base-button.component.ts +48 -0
  14. package/src/lib/shared/components/base-button/base-button.stories.ts +102 -0
  15. package/src/lib/shared/components/base-icon/base-icon.css +0 -0
  16. package/src/lib/shared/components/base-icon/base-icon.html +11 -0
  17. package/src/lib/shared/components/base-icon/base-icon.spec.ts +23 -0
  18. package/src/lib/shared/components/base-icon/base-icon.stories.ts +56 -0
  19. package/src/lib/shared/components/base-icon/base-icon.ts +18 -0
  20. package/src/lib/shared/components/base-input/base-input.css +193 -0
  21. package/src/lib/shared/components/base-input/base-input.html +43 -0
  22. package/src/lib/shared/components/base-input/base-input.spec.ts +146 -0
  23. package/src/lib/shared/components/base-input/base-input.stories.ts +137 -0
  24. package/src/lib/shared/components/base-input/base-input.ts +133 -0
  25. package/src/lib/shared/components/base-time-range/base-time-range.css +27 -0
  26. package/src/lib/shared/components/base-time-range/base-time-range.html +23 -0
  27. package/src/lib/shared/components/base-time-range/base-time-range.spec.ts +27 -0
  28. package/src/lib/shared/components/base-time-range/base-time-range.stories.ts +54 -0
  29. package/src/lib/shared/components/base-time-range/base-time-range.ts +61 -0
  30. package/src/lib/shared/components/base-toggle/base-toggle.component.html +28 -0
  31. package/src/lib/shared/components/base-toggle/base-toggle.component.scss +45 -0
  32. package/src/lib/shared/components/base-toggle/base-toggle.component.spec.ts +22 -0
  33. package/src/lib/shared/components/base-toggle/base-toggle.component.ts +138 -0
  34. package/src/lib/shared/components/base-toggle/base-toggle.stories.ts +63 -0
  35. package/src/lib/shared/components/card/card.component.css +264 -0
  36. package/src/lib/shared/components/card/card.component.html +103 -0
  37. package/src/lib/shared/components/card/card.component.ts +63 -0
  38. package/src/lib/shared/components/card/card.stories.ts +114 -0
  39. package/src/lib/shared/components/date-picker/date-picker.html +82 -0
  40. package/src/lib/shared/components/date-picker/date-picker.scss +8 -0
  41. package/src/lib/shared/components/date-picker/date-picker.spec.ts +23 -0
  42. package/src/lib/shared/components/date-picker/date-picker.stories.ts +91 -0
  43. package/src/lib/shared/components/date-picker/date-picker.ts +165 -0
  44. package/src/lib/shared/components/drop-down/drop-down.css +61 -0
  45. package/src/lib/shared/components/drop-down/drop-down.html +96 -0
  46. package/src/lib/shared/components/drop-down/drop-down.spec.ts +335 -0
  47. package/src/lib/shared/components/drop-down/drop-down.stories.ts +90 -0
  48. package/src/lib/shared/components/drop-down/drop-down.ts +335 -0
  49. package/src/lib/shared/components/footer/footer.css +29 -0
  50. package/src/lib/shared/components/footer/footer.html +10 -0
  51. package/src/lib/shared/components/footer/footer.spec.ts +22 -0
  52. package/src/lib/shared/components/footer/footer.stories.ts +51 -0
  53. package/src/lib/shared/components/footer/footer.ts +15 -0
  54. package/src/lib/shared/components/index.ts +19 -0
  55. package/src/lib/shared/components/kpi/kpi.css +0 -0
  56. package/src/lib/shared/components/kpi/kpi.html +11 -0
  57. package/src/lib/shared/components/kpi/kpi.spec.ts +38 -0
  58. package/src/lib/shared/components/kpi/kpi.stories.ts +65 -0
  59. package/src/lib/shared/components/kpi/kpi.ts +25 -0
  60. package/src/lib/shared/components/links-button/links-button.css +70 -0
  61. package/src/lib/shared/components/links-button/links-button.html +23 -0
  62. package/src/lib/shared/components/links-button/links-button.spec.ts +139 -0
  63. package/src/lib/shared/components/links-button/links-button.stories.ts +75 -0
  64. package/src/lib/shared/components/links-button/links-button.ts +45 -0
  65. package/src/lib/shared/components/paginator/paginator.css +0 -0
  66. package/src/lib/shared/components/paginator/paginator.html +10 -0
  67. package/src/lib/shared/components/paginator/paginator.spec.ts +23 -0
  68. package/src/lib/shared/components/paginator/paginator.stories.ts +56 -0
  69. package/src/lib/shared/components/paginator/paginator.ts +25 -0
  70. package/src/lib/shared/components/stepper/step/step.css +8 -0
  71. package/src/lib/shared/components/stepper/step/step.html +1 -0
  72. package/src/lib/shared/components/stepper/step/step.spec.ts +131 -0
  73. package/src/lib/shared/components/stepper/step/step.ts +14 -0
  74. package/src/lib/shared/components/stepper/stepper.css +17 -0
  75. package/src/lib/shared/components/stepper/stepper.html +11 -0
  76. package/src/lib/shared/components/stepper/stepper.spec.ts +238 -0
  77. package/src/lib/shared/components/stepper/stepper.stories.ts +77 -0
  78. package/src/lib/shared/components/stepper/stepper.ts +57 -0
  79. package/src/lib/shared/components/tab-group/tab-group.component.html +84 -0
  80. package/src/lib/shared/components/tab-group/tab-group.component.scss +151 -0
  81. package/src/lib/shared/components/tab-group/tab-group.spec.ts +22 -0
  82. package/src/lib/shared/components/tab-group/tab-group.stories.ts +102 -0
  83. package/src/lib/shared/components/tab-group/tab-group.ts +218 -0
  84. package/src/lib/shared/components/tab-panel/tab-content.directive.ts +7 -0
  85. package/src/lib/shared/components/tab-panel/tab-label.directive.ts +7 -0
  86. package/src/lib/shared/components/tab-panel/tab-panel.css +0 -0
  87. package/src/lib/shared/components/tab-panel/tab-panel.spec.ts +22 -0
  88. package/src/lib/shared/components/tab-panel/tab-panel.stories.ts +54 -0
  89. package/src/lib/shared/components/tab-panel/tab-panel.ts +31 -0
  90. package/src/lib/shared/components/time-picker/time-picker.css +84 -0
  91. package/src/lib/shared/components/time-picker/time-picker.html +21 -0
  92. package/src/lib/shared/components/time-picker/time-picker.spec.ts +23 -0
  93. package/src/lib/shared/components/time-picker/time-picker.stories.ts +66 -0
  94. package/src/lib/shared/components/time-picker/time-picker.ts +90 -0
  95. package/types/ngx-opalbytes-components.d.ts +23 -4
@@ -0,0 +1,102 @@
1
+ import type { Meta, StoryObj } from '@storybook/angular-vite';
2
+ import { fn } from 'storybook/test';
3
+
4
+ import { CaoTabGroup } from './tab-group';
5
+ import { CaoTabPanel } from '../tab-panel/tab-panel';
6
+
7
+ const renderTabs = (panels: string) => ({
8
+ template: `<cao-tab-group>${panels}</cao-tab-group>`,
9
+ moduleMetadata: { imports: [CaoTabGroup, CaoTabPanel] },
10
+ });
11
+
12
+ const defaultPanels = `
13
+ <cao-tab-panel label="Aba 1"><p>Conteúdo da primeira aba.</p></cao-tab-panel>
14
+ <cao-tab-panel label="Aba 2"><p>Conteúdo da segunda aba.</p></cao-tab-panel>
15
+ <cao-tab-panel label="Aba 3"><p>Conteúdo da terceira aba.</p></cao-tab-panel>
16
+ `;
17
+
18
+ const panelsWithIcons = `
19
+ <cao-tab-panel label="Início" leadingIcon="Home"><p>Conteúdo de início.</p></cao-tab-panel>
20
+ <cao-tab-panel label="Buscar" leadingIcon="Search"><p>Conteúdo de busca.</p></cao-tab-panel>
21
+ <cao-tab-panel label="Configurações" leadingIcon="Settings"><p>Conteúdo de configurações.</p></cao-tab-panel>
22
+ `;
23
+
24
+ const meta: Meta<CaoTabGroup> = {
25
+ title: 'Componentes/Tab Group',
26
+ component: CaoTabGroup,
27
+ tags: ['autodocs'],
28
+ argTypes: {
29
+ selectedIndex: { control: { type: 'number', min: 0 } },
30
+ labelHeight: { control: 'text' },
31
+ labelPaddingX: { control: 'text' },
32
+ labelGap: { control: 'text' },
33
+ colorText: { control: 'color' },
34
+ colorActive: { control: 'color' },
35
+ colorHoverBg: { control: 'color' },
36
+ colorDisabled: { control: 'color' },
37
+ colorInkBar: { control: 'color' },
38
+ colorHeaderBorder: { control: 'color' },
39
+ inkBarHeight: { control: 'text' },
40
+ contentPaddingTop: { control: 'text' },
41
+ labelRadius: { control: 'text' },
42
+ animationDuration: { control: 'text' },
43
+ dynamicHeight: { control: 'boolean' },
44
+ labelAlign: { control: 'select', options: ['start', 'center', 'end'] },
45
+ iconColorActive: { control: 'color' },
46
+ iconColorInactive: { control: 'color' },
47
+ strokeWidthIcon: { control: { type: 'number', min: 0.5, max: 3, step: 0.1 } },
48
+ },
49
+ args: {
50
+ selectedTabChange: fn(),
51
+ focusChange: fn(),
52
+ },
53
+ };
54
+
55
+ export default meta;
56
+ type Story = StoryObj<CaoTabGroup>;
57
+
58
+ export const Padrao: Story = {
59
+ render: () => renderTabs(defaultPanels),
60
+ };
61
+
62
+ export const ComIcones: Story = {
63
+ render: () => renderTabs(panelsWithIcons),
64
+ };
65
+
66
+ export const ComAbaDesabilitada: Story = {
67
+ args: {
68
+ selectedIndex: 0,
69
+ },
70
+ render: () =>
71
+ renderTabs(`
72
+ <cao-tab-panel label="Aba 1"><p>Conteúdo da primeira aba.</p></cao-tab-panel>
73
+ <cao-tab-panel label="Bloqueada" isDisabled="true"><p>Não acessível.</p></cao-tab-panel>
74
+ <cao-tab-panel label="Aba 3"><p>Conteúdo da terceira aba.</p></cao-tab-panel>
75
+ `),
76
+ };
77
+
78
+ export const CoresPersonalizadas: Story = {
79
+ args: {
80
+ colorText: '#374151',
81
+ colorActive: '#dc2626',
82
+ colorHoverBg: '#fee2e2',
83
+ colorInkBar: '#dc2626',
84
+ colorHeaderBorder: '#f3f4f6',
85
+ },
86
+ render: () => renderTabs(defaultPanels),
87
+ };
88
+
89
+ export const LabelsCentralizados: Story = {
90
+ args: {
91
+ labelAlign: 'center',
92
+ },
93
+ render: () => renderTabs(defaultPanels),
94
+ };
95
+
96
+ export const TrailingIcons: Story = {
97
+ render: () =>
98
+ renderTabs(`
99
+ <cao-tab-panel label="Notificações" trailingIcon="Bell"><p>Conteúdo com sino.</p></cao-tab-panel>
100
+ <cao-tab-panel label="Perfil" trailingIcon="User"><p>Conteúdo de perfil.</p></cao-tab-panel>
101
+ `),
102
+ };
@@ -0,0 +1,218 @@
1
+ import { isPlatformBrowser, NgTemplateOutlet } from '@angular/common';
2
+ import {
3
+ Component,
4
+ ContentChildren,
5
+ QueryList,
6
+ AfterContentInit,
7
+ OnDestroy,
8
+ Input,
9
+ Output,
10
+ EventEmitter,
11
+ signal,
12
+ WritableSignal,
13
+ inject,
14
+ PLATFORM_ID,
15
+ ElementRef,
16
+ ViewChildren,
17
+ ViewChild,
18
+ HostBinding,
19
+ } from '@angular/core';
20
+
21
+ import { LucideDynamicIcon } from '@lucide/angular';
22
+ import { Subscription } from 'rxjs';
23
+
24
+ import { CaoTabPanel } from '../tab-panel/tab-panel';
25
+
26
+
27
+ export interface ICaoTabChangeEvent {
28
+ previousIndex: number;
29
+ index: number;
30
+ tab: CaoTabPanel;
31
+ }
32
+
33
+ @Component({
34
+ selector: 'cao-tab-group',
35
+ standalone: true,
36
+ imports: [NgTemplateOutlet, LucideDynamicIcon],
37
+ templateUrl: './tab-group.component.html',
38
+ styleUrl: './tab-group.component.scss',
39
+ host: { class: 'cao-tab-group' },
40
+ })
41
+ export class CaoTabGroup implements AfterContentInit, OnDestroy {
42
+ @HostBinding('style')
43
+ get _hostStyles(): Record<string, string> {
44
+ return {
45
+ '--cao-tab-label-height': this.labelHeight,
46
+ '--cao-tab-label-px': this.labelPaddingX,
47
+ '--cao-tab-label-gap': this.labelGap,
48
+ '--cao-tab-color-text': this.colorText,
49
+ '--cao-tab-color-active': this.colorActive,
50
+ '--cao-tab-color-hover-bg': this.colorHoverBg,
51
+ '--cao-tab-color-disabled': this.colorDisabled,
52
+ '--cao-tab-color-ink-bar': this.colorInkBar,
53
+ '--cao-tab-color-header-border': this.colorHeaderBorder,
54
+ '--cao-tab-ink-bar-height': this.inkBarHeight,
55
+ '--cao-tab-content-pt': this.contentPaddingTop,
56
+ '--cao-tab-radius': this.labelRadius,
57
+ };
58
+ }
59
+ @Input()
60
+ set selectedIndex(val: number) {
61
+ this.selectIndex(val, false);
62
+ }
63
+ get selectedIndex(): number {
64
+ return this.activeIndex();
65
+ }
66
+
67
+ @Input() labelHeight = '48px';
68
+ @Input() labelPaddingX = '16px';
69
+ @Input() labelGap = '6px';
70
+ @Input() colorText = '#6b7280';
71
+ @Input() colorActive = '#4f46e5';
72
+ @Input() colorHoverBg = '#f3f4f6';
73
+ @Input() colorDisabled = '#d1d5db';
74
+ @Input() colorInkBar = '#4f46e5';
75
+ @Input() colorHeaderBorder = '#e5e7eb';
76
+ @Input() inkBarHeight = '2px';
77
+ @Input() contentPaddingTop = '20px';
78
+ @Input() labelRadius = '4px';
79
+ @Input() animationDuration = '300ms';
80
+ @Input() dynamicHeight = false;
81
+
82
+ @Input() labelAlign: 'start' | 'center' | 'end' = 'start';
83
+ @Input() iconColorActive = ''
84
+ @Input() iconColorInactive = ''
85
+ @Input() strokeWidthIcon = 1
86
+
87
+ @Output() readonly selectedTabChange = new EventEmitter<ICaoTabChangeEvent>();
88
+
89
+ @Output() readonly focusChange = new EventEmitter<number>();
90
+
91
+ @ContentChildren(CaoTabPanel) tabs!: QueryList<CaoTabPanel>;
92
+ @ViewChildren('tabBtn') tabBtns!: QueryList<ElementRef<HTMLButtonElement>>;
93
+ @ViewChild('inkBar') inkBar!: ElementRef<HTMLElement>;
94
+
95
+ readonly activeIndex: WritableSignal<number> = signal(0);
96
+
97
+ private activatedSet = new Set<number>();
98
+
99
+ tabList: CaoTabPanel[] = [];
100
+
101
+ private subs = new Subscription();
102
+ private isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
103
+
104
+ ngAfterContentInit(): void {
105
+ this.tabList = this.tabs.toArray();
106
+ this.activatedSet.add(this.activeIndex());
107
+
108
+ this.subs.add(
109
+ this.tabs.changes.subscribe(() => {
110
+ this.tabList = this.tabs.toArray();
111
+ const safe = Math.min(this.activeIndex(), this.tabList.length - 1);
112
+ this.activeIndex.set(Math.max(0, safe));
113
+ this.scheduleInkBar();
114
+ })
115
+ );
116
+
117
+ this.scheduleInkBar();
118
+ }
119
+
120
+ ngOnDestroy(): void {
121
+ this.subs.unsubscribe();
122
+ }
123
+
124
+ selectTab(index: number): void {
125
+ this.selectIndex(index, true);
126
+ }
127
+
128
+ get activeTab(): CaoTabPanel | null {
129
+ return this.tabList[this.activeIndex()] ?? null;
130
+ }
131
+
132
+ isActive(index: number): boolean {
133
+ return this.activeIndex() === index;
134
+ }
135
+ wasActivated(index: number): boolean {
136
+ return this.activatedSet.has(index);
137
+ }
138
+
139
+ onTabClick(index: number): void {
140
+ if (this.tabList[index]?.isDisabled) {return;}
141
+ this.selectIndex(index, true);
142
+ }
143
+
144
+ onKeydown(event: KeyboardEvent, currentIndex: number): void {
145
+ let next = currentIndex;
146
+
147
+ switch (event.key) {
148
+ case 'ArrowRight': next = this.nextEnabled(currentIndex, 1); break;
149
+ case 'ArrowLeft': next = this.nextEnabled(currentIndex, -1); break;
150
+ case 'Home': next = this.nextEnabled(-1, 1); break;
151
+ case 'End': next = this.nextEnabled(this.tabList.length, -1); break;
152
+ default: return;
153
+ }
154
+
155
+ event.preventDefault();
156
+ this.selectIndex(next, true);
157
+ this.focusTab(next);
158
+ }
159
+
160
+ onFocus(index: number): void {
161
+ this.focusChange.emit(index);
162
+ }
163
+
164
+ private nextEnabled(from: number, dir: 1 | -1): number {
165
+ const tabs = this.tabList;
166
+ let idx = from + dir;
167
+ while (idx >= 0 && idx < tabs.length) {
168
+ if (!tabs[idx].isDisabled) {return idx;}
169
+ idx += dir;
170
+ }
171
+ return from;
172
+ }
173
+
174
+ private selectIndex(index: number, emit: boolean): void {
175
+ if (!this.tabList) {
176
+ this.activeIndex.set(Math.max(0, index));
177
+ return;
178
+ }
179
+ const clamped = Math.max(0, Math.min(index, this.tabList.length - 1));
180
+ const previous = this.activeIndex();
181
+ if (clamped === previous && emit) {return;}
182
+
183
+ this.activeIndex.set(clamped);
184
+ this.activatedSet.add(clamped);
185
+
186
+ if (emit) {
187
+ this.selectedTabChange.emit({
188
+ previousIndex: previous,
189
+ index: clamped,
190
+ tab: this.tabList[clamped],
191
+ });
192
+ }
193
+
194
+ this.scheduleInkBar();
195
+ }
196
+
197
+ private focusTab(index: number): void {
198
+ this.tabBtns?.toArray()[index]?.nativeElement?.focus();
199
+ }
200
+
201
+ scheduleInkBar(): void {
202
+ if (!this.isBrowser) {return;}
203
+ requestAnimationFrame(() => this.updateInkBar());
204
+ }
205
+
206
+ private updateInkBar(): void {
207
+ const btns = this.tabBtns?.toArray();
208
+ const bar = this.inkBar?.nativeElement;
209
+ if (!btns || !bar) {return;}
210
+
211
+ const btn = btns[this.activeIndex()]?.nativeElement;
212
+ if (!btn) {return;}
213
+
214
+ bar.style.width = `${btn.offsetWidth}px`;
215
+ bar.style.transform = `translateX(${btn.offsetLeft}px)`;
216
+ bar.style.transitionDuration = this.animationDuration;
217
+ }
218
+ }
@@ -0,0 +1,7 @@
1
+ import { Directive } from '@angular/core';
2
+
3
+ @Directive({
4
+ selector: '[caoTabContent]',
5
+ standalone: true,
6
+ })
7
+ export class CaoTabContentDirective {}
@@ -0,0 +1,7 @@
1
+ import { Directive } from '@angular/core';
2
+
3
+ @Directive({
4
+ selector: '[caoTabLabel]',
5
+ standalone: true,
6
+ })
7
+ export class CaoTabLabelDirective {}
@@ -0,0 +1,22 @@
1
+ import { ComponentFixture, TestBed } from "@angular/core/testing";
2
+
3
+ import { CaoTabPanel } from "./tab-panel";
4
+
5
+ describe("CaoTabPanel", () => {
6
+ let component: CaoTabPanel;
7
+ let fixture: ComponentFixture<CaoTabPanel>;
8
+
9
+ beforeEach(async () => {
10
+ await TestBed.configureTestingModule({
11
+ imports: [CaoTabPanel],
12
+ }).compileComponents();
13
+
14
+ fixture = TestBed.createComponent(CaoTabPanel);
15
+ component = fixture.componentInstance;
16
+ await fixture.whenStable();
17
+ });
18
+
19
+ it("should create", () => {
20
+ expect(component).toBeTruthy();
21
+ });
22
+ });
@@ -0,0 +1,54 @@
1
+ import type { Meta, StoryObj } from '@storybook/angular-vite';
2
+
3
+ import { CaoTabPanel } from './tab-panel';
4
+
5
+ const meta: Meta<CaoTabPanel> = {
6
+ title: 'Componentes/Tab Panel',
7
+ component: CaoTabPanel,
8
+ tags: ['autodocs'],
9
+ argTypes: {
10
+ label: { control: 'text' },
11
+ isLucideIcon: { control: 'boolean' },
12
+ leadingIcon: { control: 'text' },
13
+ trailingIcon: { control: 'text' },
14
+ isDisabled: { control: 'boolean' },
15
+ },
16
+ args: {
17
+ label: 'Conteúdo da aba',
18
+ },
19
+ };
20
+
21
+ export default meta;
22
+ type Story = StoryObj<CaoTabPanel>;
23
+
24
+ export const Padrao: Story = {
25
+ render: (args) => ({
26
+ template: `
27
+ <cao-tab-panel
28
+ [label]="label"
29
+ [isLucideIcon]="isLucideIcon"
30
+ [leadingIcon]="leadingIcon"
31
+ [trailingIcon]="trailingIcon"
32
+ [isDisabled]="isDisabled">
33
+ <p>Este é o conteúdo renderizado pela aba.</p>
34
+ </cao-tab-panel>
35
+ `,
36
+ props: args,
37
+ }),
38
+ };
39
+
40
+ export const ComLeadingIcon: Story = {
41
+ args: { label: 'Início', leadingIcon: 'Home', isLucideIcon: true },
42
+ };
43
+
44
+ export const ComTrailingIcon: Story = {
45
+ args: { label: 'Notificações', trailingIcon: 'Bell', isLucideIcon: true },
46
+ };
47
+
48
+ export const ComAmbosIcones: Story = {
49
+ args: { label: 'Configurações', leadingIcon: 'Settings', trailingIcon: 'ChevronRight', isLucideIcon: true },
50
+ };
51
+
52
+ export const Desabilitado: Story = {
53
+ args: { label: 'Bloqueada', isDisabled: true },
54
+ };
@@ -0,0 +1,31 @@
1
+ import { Component, ContentChild, Input, TemplateRef, ViewChild } from "@angular/core"
2
+
3
+ import { CaoTabContentDirective } from "./tab-content.directive"
4
+ import { CaoTabLabelDirective } from "./tab-label.directive"
5
+
6
+ @Component({
7
+ selector: "cao-tab-panel",
8
+ imports: [],
9
+ template: `
10
+ <ng-template #implicitContent>
11
+ <ng-content />
12
+ </ng-template>
13
+ `,
14
+ styleUrl: "./tab-panel.css",
15
+ })
16
+ export class CaoTabPanel {
17
+ @Input() label = ''
18
+ @Input() isLucideIcon = true
19
+ @Input() trailingIcon: string | null = null
20
+ @Input() leadingIcon: string | null = null
21
+ @Input() isDisabled = false
22
+
23
+ @ContentChild(CaoTabLabelDirective, { read: TemplateRef })
24
+ labelTemplate?: TemplateRef<unknown>;
25
+
26
+ @ContentChild(CaoTabContentDirective, { read: TemplateRef })
27
+ lazyContent?: TemplateRef<unknown>;
28
+
29
+ @ViewChild('implicitContent', { static: true })
30
+ content!: TemplateRef<unknown>;
31
+ }
@@ -0,0 +1,84 @@
1
+ .time-picker-wrapper {
2
+ width: 130px;
3
+ display: flex;
4
+ flex-direction: column;
5
+ position: relative;
6
+ }
7
+
8
+ .custom-timepicker {
9
+ width: 100%;
10
+ }
11
+
12
+ .custom-timepicker .mat-mdc-text-field-wrapper {
13
+ border: 1px solid transparent;
14
+ border-radius: 8px;
15
+ padding: 0;
16
+ height: 43px;
17
+ display: flex;
18
+ align-items: center;
19
+ background-color: var(--input-background);
20
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
21
+ transition: border-color 0.2s ease-in-out;
22
+ }
23
+
24
+ .custom-timepicker.invalid .mat-mdc-text-field-wrapper {
25
+ border-color: var(--error);
26
+ }
27
+
28
+ /* só aplica o verde de success enquanto estiver focado */
29
+ .custom-timepicker.valid.mat-focused .mat-mdc-text-field-wrapper {
30
+ border-color: var(--success);
31
+ }
32
+
33
+ /* após perder o foco, "neutraliza" o success para transparente */
34
+ .custom-timepicker.valid:not(.mat-focused) .mat-mdc-text-field-wrapper {
35
+ border-color: transparent;
36
+ }
37
+
38
+ .custom-timepicker .mdc-line-ripple {
39
+ display: none;
40
+ }
41
+
42
+ .custom-timepicker .mat-mdc-input-element {
43
+ font-size: 16px;
44
+ padding-left: 16px;
45
+ caret-color: transparent;
46
+ background: transparent;
47
+ }
48
+
49
+ .custom-timepicker .mat-mdc-icon-button.mat-mdc-button-base {
50
+ padding: 0;
51
+ border-left: 1px solid var(--line-color);
52
+ border-radius: 0;
53
+ margin: 0;
54
+ display: flex;
55
+ align-items: center;
56
+ justify-content: center;
57
+ }
58
+
59
+ .custom-timepicker .mat-timepicker-toggle-default-icon {
60
+ color: var(--primary);
61
+ }
62
+
63
+ .custom-timepicker .mat-mdc-form-field-flex {
64
+ background-color: var(--input-background);
65
+ }
66
+
67
+ /* oculta label flutuante e subscript conforme seu estilo */
68
+ .mat-mdc-form-field:not(.mat-form-field-disabled) .mat-mdc-floating-label.mdc-floating-label {
69
+ display: none;
70
+ }
71
+
72
+ .mat-mdc-form-field-subscript-wrapper,
73
+ .mat-mdc-form-field-bottom-align::before {
74
+ display: none;
75
+ }
76
+
77
+ .custom-timepicker .mat-mdc-form-field-infix {
78
+ padding: 18px 0 !important;
79
+ }
80
+
81
+ /* remove qualquer box-shadow remanescente no outline quando focado */
82
+ .custom-timepicker.mat-form-field-appearance-outline.mat-focused .mat-form-field-outline-thick {
83
+ box-shadow: none !important;
84
+ }
@@ -0,0 +1,21 @@
1
+ <div class="time-picker-wrapper">
2
+ <mat-form-field
3
+ class="custom-timepicker"
4
+ [ngClass]="{
5
+ 'invalid': control?.touched && control?.invalid,
6
+ 'valid': control?.touched && control?.valid
7
+ }">
8
+ <input
9
+ matInput
10
+ placeholder="00:00"
11
+ [formControl]="control"
12
+ [matTimepicker]="picker"
13
+ [min]="min"
14
+ [max]="max"
15
+ [disabled]="isDisabled"
16
+ [attr.data-cy]="dataCy"
17
+ (blur)="control?.markAsTouched()" />
18
+ <mat-timepicker-toggle matSuffix [for]="picker" />
19
+ <mat-timepicker #picker [options]="customOptions" (selected)="timeChange.emit($event.value)" />
20
+ </mat-form-field>
21
+ </div>
@@ -0,0 +1,23 @@
1
+ import { ComponentFixture, TestBed } from '@angular/core/testing';
2
+
3
+ import { TimePicker } from './time-picker';
4
+
5
+ describe('TimePicker', () => {
6
+ let component: TimePicker;
7
+ let fixture: ComponentFixture<TimePicker>;
8
+
9
+ beforeEach(async () => {
10
+ await TestBed.configureTestingModule({
11
+ imports: [TimePicker]
12
+ })
13
+ .compileComponents();
14
+
15
+ fixture = TestBed.createComponent(TimePicker);
16
+ component = fixture.componentInstance;
17
+ await fixture.whenStable();
18
+ });
19
+
20
+ it('should create', () => {
21
+ expect(component).toBeTruthy();
22
+ });
23
+ });
@@ -0,0 +1,66 @@
1
+ import { FormControl } from '@angular/forms';
2
+
3
+ import type { Meta, StoryObj } from '@storybook/angular-vite';
4
+ import { fn } from 'storybook/test';
5
+
6
+ import { TimePicker } from './time-picker';
7
+
8
+ const meta: Meta<TimePicker> = {
9
+ title: 'Componentes/Time Picker',
10
+ component: TimePicker,
11
+ tags: ['autodocs'],
12
+ argTypes: {
13
+ isDisabled: { control: 'boolean' },
14
+ dataCy: { control: 'text' },
15
+ min: { control: 'text' },
16
+ max: { control: 'text' },
17
+ },
18
+ args: {
19
+ timeChange: fn(),
20
+ min: '07:00',
21
+ max: '23:00',
22
+ },
23
+ };
24
+
25
+ export default meta;
26
+ type Story = StoryObj<TimePicker>;
27
+
28
+ export const Padrao: Story = {
29
+ args: {
30
+ min: '07:00',
31
+ max: '23:00',
32
+ },
33
+ };
34
+
35
+ export const ComMinimoEMaximo: Story = {
36
+ args: {
37
+ min: '09:00',
38
+ max: '18:00',
39
+ },
40
+ };
41
+
42
+ export const ComValor: Story = {
43
+ render: (args) => ({
44
+ template: `
45
+ <cao-time-picker
46
+ [control]="control"
47
+ [min]="min"
48
+ [max]="max"
49
+ [isDisabled]="isDisabled"
50
+ (timeChange)="timeChange($event)" />
51
+ `,
52
+ props: {
53
+ control: new FormControl<Date | null>(new Date(2026, 0, 1, 14, 30)),
54
+ min: args.min,
55
+ max: args.max,
56
+ isDisabled: args.isDisabled,
57
+ timeChange: fn(),
58
+ },
59
+ }),
60
+ };
61
+
62
+ export const Desabilitado: Story = {
63
+ args: {
64
+ isDisabled: true,
65
+ },
66
+ };