Skip to content
Closed
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
08e946f
feat(context-menu): implementa componente po-context-menu
anderson-gregorio-totvs Mar 11, 2026
0d55218
fix(context-menu): corrige bugs e aplica melhorias de clean code
anderson-gregorio-totvs Mar 19, 2026
b9eb6df
feat(context-menu): implementa componente po-context-menu
anderson-gregorio-totvs Mar 19, 2026
547163e
feat(context-menu): implementa componente po-context-menu
anderson-gregorio-totvs Mar 19, 2026
05ed32e
feat(context-menu): adiciona exemplos basic e labs do po-context-menu
anderson-gregorio-totvs Mar 19, 2026
b80eb03
feat(context-menu): adiciona exemplos basic e labs do po-context-menu
anderson-gregorio-totvs Mar 23, 2026
0789567
build(docs): ajusta a geração da doc para output em signal
anderson-gregorio-totvs Mar 23, 2026
96d3059
feat(tabs): adiciona novas funcionalidades
anderson-gregorio-totvs Mar 23, 2026
d9030ac
docs(tabs): documenta activatedTab e focusTab, corrige NG0203 no spec
anderson-gregorio-totvs Mar 23, 2026
4f68058
fix(tabs): adiciona guard contra id inexistente em focusTab
anderson-gregorio-totvs Mar 23, 2026
ea91358
fix(docs): corrige inferência de tipo number para signals sem argumento
anderson-gregorio-totvs Mar 23, 2026
09691c2
fix(tabs): adiciona activatedTab nos mocks de teste e corrige typos n…
anderson-gregorio-totvs Mar 23, 2026
2f7c815
fix(context-menu): preventDefault no space e regex input.required
anderson-gregorio-totvs Mar 23, 2026
c562a95
feat(context-menu): implementa componente po-context-menu
anderson-gregorio-totvs Mar 11, 2026
02a467d
feat(tabs): adiciona novas funcionalidades
anderson-gregorio-totvs Mar 23, 2026
2bec47e
build(docs): ajusta a geração da doc para output em signal
anderson-gregorio-totvs Mar 23, 2026
18ec322
fix(context-menu): corrige evento expandedChange e regex generics
anderson-gregorio-totvs Mar 23, 2026
7460498
merge: resolve conflitos mantendo fixes locais
anderson-gregorio-totvs Mar 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions projects/portal/docs/configuration.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,6 @@ module.exports = {
{ name: 'usedBy' },
{ name: 'optional' },
{ name: 'default' },
{ name: "Output" },
{ name: "Input" },
{ name: 'override' }
],

Expand Down
44 changes: 22 additions & 22 deletions projects/portal/docs/processors/helpers/functions.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ module.exports = {

// Resolve all methods and properties from the classDoc. Includes inherited docs.
classDoc.methods = classDoc.methods.concat(this.resolveMethods(classDoc));

// Concatena propriedades da classe atual
const currentProperties = this.resolveProperties(classDoc);
classDoc.properties = classDoc.properties.concat(currentProperties);
Expand All @@ -55,14 +55,14 @@ module.exports = {
propertyMap.set(prop.name, prop);
continue;
}

const hasOverrideTag = (prop.tags?.tags || []).some(tag => tag.tagName === 'override');

if (hasOverrideTag) {
propertyMap.set(prop.name, prop);
}
}

// Converte o Map de volta para array e ordena
classDoc.properties = Array.from(propertyMap.values()).sort((a, b) => {
return a.name > b.name ? 1 : -1;
Expand Down Expand Up @@ -95,21 +95,11 @@ module.exports = {
processPropertyDoc: function (propertyDoc) {
this.processPublicDoc(propertyDoc);

if (propertyDoc.Input != undefined) {
propertyDoc.isDirectiveInput = true;
propertyDoc.directiveInputAlias = propertyDoc.directiveInputAlias || propertyDoc.Input;
} else {
propertyDoc.isDirectiveInput = this.isDirectiveInput(propertyDoc);
propertyDoc.directiveInputAlias = this.getDirectiveInputAlias(propertyDoc);
}
propertyDoc.isDirectiveInput = this.isDirectiveInput(propertyDoc) || propertyDoc.isDirectiveInput;
propertyDoc.directiveInputAlias = this.getDirectiveInputAlias(propertyDoc) || propertyDoc.directiveInputAlias;

if (propertyDoc.Output != undefined) {
propertyDoc.isDirectiveOutput = true;
propertyDoc.directiveOutputAlias = propertyDoc.Output;
} else {
propertyDoc.isDirectiveOutput = this.isDirectiveOutput(propertyDoc);
propertyDoc.directiveOutputAlias = this.getDirectiveOutputAlias(propertyDoc);
}
propertyDoc.isDirectiveOutput = this.isDirectiveOutput(propertyDoc) || propertyDoc.isDirectiveOutput;
propertyDoc.directiveOutputAlias = this.getDirectiveOutputAlias(propertyDoc) || propertyDoc.directiveOutputAlias;
},

/**
Expand All @@ -130,13 +120,13 @@ module.exports = {
resolveProperties: function (classDoc) {
let properties = classDoc.members.filter(member => !member.hasOwnProperty('parameters'));

const inferTypeFromInput = codeLine => {
const inferTypeFromCode = codeLine => {
// Extrai tipo genérico
const genericMatch = codeLine.match(/input<([^>]+)>/);
const genericMatch = codeLine.match(/(?:input|output|model)<([^>]+)>/);
Copy link
Contributor

@devin-ai-integration devin-ai-integration bot Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Greedy .+ in generic type regex truncates nested generic types like Array<PoContextMenuItem>

The regex /(?:input(?:\.required)?|output|model)<(.+)>(?=\s*\()/ at projects/portal/docs/processors/helpers/functions.js:125 uses greedy .+ to capture the generic type parameter. For nested generics, the greedy quantifier backtracks to find the > immediately before ( (due to the lookahead), which is the outermost closing > — causing the captured group to miss the inner closing >. This directly impacts the new PoContextMenuBaseComponent's items property defined as input<Array<PoContextMenuItem>>([], { alias: 'p-items' }): the type captured would be Array<PoContextMenuItem instead of Array<PoContextMenuItem>. The fix is to use non-greedy .+?, which correctly finds the first > that satisfies the (?=\s*\() lookahead.

Detailed trace for input<Array<PoContextMenuItem>>([])

With greedy .+:

  • .+ = Array<PoContextMenuItem>> matches second > → lookahead sees ( → capture: Array<PoContextMenuItem

With non-greedy .+?:

  • .+? = Array<PoContextMenuItem> matches first > → lookahead sees > not ( → fail, continue
  • .+? = Array<PoContextMenuItem>> matches second > → lookahead sees ( → capture: Array<PoContextMenuItem>
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 18ec322 — updated both regex patterns to use .+ with lookahead (?=\s*\() instead of [^>]+, so nested generics like Array<PoContextMenuItem> and string | TemplateRef<void> are captured correctly.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The greedy .+ actually works correctly here. The lookahead (?=\s*\() constrains the match — the regex engine backtracks from the right until it finds a > followed by (, which is always the outermost closing >.\n\nTrace for input<Array<PoContextMenuItem>>([], ...):\n- .+ greedily matches everything, then backtracks\n- Finds > at position right before ( (the second >)\n- .+ = Array<PoContextMenuItem>> matches second > → lookahead sees ( → capture: Array<PoContextMenuItem> ✅\n\nBoth greedy and non-greedy produce identical results for all signal patterns because the >(?=\s*\() anchor forces the correct match point. No change needed.

const genericRaw = genericMatch ? genericMatch[1].trim() : null;

// Extrai valor passado como primeiro argumento
const valueMatch = codeLine.match(/input(?:<[^>]+>)?\(\s*([^,)]*)/);
const valueMatch = codeLine.match(/(?:input|output|model)(?:<[^>]+>)?\(\s*([^,)]*)/);
const value = valueMatch ? valueMatch[1].trim() : '';

// Extrai alias (entre aspas simples ou duplas)
Expand Down Expand Up @@ -167,10 +157,20 @@ module.exports = {
property.isOptional = true;
}

if (property.type.includes('input') && property.Input !== undefined) {
const { type, alias } = inferTypeFromInput(property.type);
// Tratamento input e model no Signal
if ((property.type.includes('input') || property.type.includes('model'))) {
const { type, alias } = inferTypeFromCode(property.type);
properties[index].type = type;
properties[index].directiveInputAlias = alias;
properties[index].isDirectiveInput = true;
}
Comment on lines 157 to +166
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Docs processor does not classify model() signal properties as outputs

Angular's model() signal creates a two-way binding that acts as both an input and an output. In resolveProperties at projects/portal/docs/processors/helpers/functions.js:161-166, properties whose type includes model are only classified as inputs (isDirectiveInput = true), but isDirectiveOutput is never set. The output check at line 169 (property.type.includes('output')) does not match model. This means the expanded property in PoContextMenuBaseComponent (which uses model<boolean>) will not be documented as supporting two-way binding output events.

Suggested change
property.isOptional = true;
}
if (property.type.includes('input') && property.Input !== undefined) {
const { type, alias } = inferTypeFromInput(property.type);
// Tratamento input e model no Signal
if ((property.type.includes('input') || property.type.includes('model'))) {
const { type, alias } = inferTypeFromCode(property.type);
properties[index].type = type;
properties[index].directiveInputAlias = alias;
properties[index].isDirectiveInput = true;
}
// Tratamento input e model no Signal
if ((property.type.includes('input') || property.type.includes('model'))) {
const { type, alias } = inferTypeFromCode(property.type);
properties[index].type = type;
properties[index].directiveInputAlias = alias;
properties[index].isDirectiveInput = true;
}
// model() also acts as an output for two-way binding
if (property.type.includes('model')) {
const { alias } = inferTypeFromCode(property.type);
properties[index].directiveOutputAlias = alias ? alias + 'Change' : null;
properties[index].isDirectiveOutput = true;
}
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.


// Tratamento output no Signal
if (property.type.includes('output')) {
const { type, alias } = inferTypeFromCode(property.type);
properties[index].type = 'EventEmitter';
properties[index].directiveOutputAlias = alias;
properties[index].isDirectiveOutput = true;
}
});

Expand Down
2 changes: 2 additions & 0 deletions projects/ui/src/lib/components/components.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { PoButtonModule } from './po-button/po-button.module';
import { PoCalendarModule } from './po-calendar/po-calendar.module';
import { PoChartModule } from './po-chart/po-chart.module';
import { PoContainerModule } from './po-container/po-container.module';
import { PoContextMenuModule } from './po-context-menu/po-context-menu.module';
import { PoDisclaimerGroupModule } from './po-disclaimer-group/po-disclaimer-group.module';
import { PoDisclaimerModule } from './po-disclaimer/po-disclaimer.module';
import { PoDividerModule } from './po-divider/po-divider.module';
Expand Down Expand Up @@ -61,6 +62,7 @@ const PO_MODULES = [
PoCalendarModule,
PoChartModule,
PoContainerModule,
PoContextMenuModule,
PoDisclaimerGroupModule,
PoDisclaimerModule,
PoDividerModule,
Expand Down
1 change: 1 addition & 0 deletions projects/ui/src/lib/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export * from './po-button/index';
export * from './po-calendar/index';
export * from './po-chart/index';
export * from './po-container/index';
export * from './po-context-menu/index';
export * from './po-disclaimer-group/index';
export * from './po-disclaimer/index';
export * from './po-divider/index';
Expand Down
3 changes: 3 additions & 0 deletions projects/ui/src/lib/components/po-context-menu/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './po-context-menu-item.interface';
export * from './po-context-menu.component';
export * from './po-context-menu.module';
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { Directive, input, model, output } from '@angular/core';

import { PoContextMenuItem } from './po-context-menu-item.interface';

/**
* @description
*
* O componente `po-context-menu` e uma barra lateral de contexto (sidebar) para navegacao interna de modulos.
* Inspirado visualmente no `po-menu`, porem independente e focado em navegacao contextual.
*
* Quando estiver sendo utilizado o componente po-page-default, ambos devem estar no mesmo nível
* e inseridos em uma div com a classe **po-context-menu-wrapper**.
* Esta classe será responsável por fazer os cálculos necessários de alinhamento dos componentes.
*
* #### Tokens customizaveis
*
* E possivel alterar o estilo do componente usando os seguintes tokens (CSS):
*
* > Para maiores informacoes, acesse o guia [Personalizando o Tema Padrao com Tokens CSS](https://po-ui.io/guides/theme-customization).
*
* | Propriedade | Descricao | Valor Padrao |
* |----------------------------------------|--------------------------------------------------------------|-------------------------------------------------|
* | **Default Values** | | |
* | `--font-family` | Familia tipografica usada | `var(--font-family-theme)` |
* | `--font-size` | Tamanho da fonte dos itens | `var(--font-size-default)` |
* | `--font-size-context-title` | Tamanho da fonte do titulo de contexto | `var(--font-size-sm)` |
* | `--font-size-title` | Tamanho da fonte do titulo principal | `var(--font-size-lg)` |
* | `--line-height` | Altura da linha | `var(--line-height-md)` |
* | `--border-radius` | Raio dos cantos dos itens | `var(--border-radius-md)` |
* | `--border-color` | Cor da borda lateral direita do componente | `var(--color-neutral-light-20)` |
* | `--background-color` | Cor de fundo do componente | `var(--color-neutral-light-05)` |
* | `--color` | Cor do texto dos itens | `var(--color-action-default)` |
* | `--color-context-title` | Cor do texto do titulo de contexto | `var(--color-neutral-mid-40)` |
* | `--color-title` | Cor do texto do titulo principal | `var(--color-neutral-dark-80)` |
* | `--font-weight` | Peso da fonte dos itens | `var(--font-weight-bold)` |
* | `--font-weight-title` | Peso da fonte do titulo principal | `var(--font-weight-bold)` |
* | `--outline-color-focused` | Cor do outline no estado de focus | `var(--color-action-focus)` |
* | **Hover** | | |
* | `--color-hover` | Cor do texto no estado hover | `var(--color-brand-01-darkest)` |
* | `--background-color-hover` | Cor de fundo no estado hover | `var(--color-brand-01-lighter)` |
* | **Pressed** | | |
* | `--background-color-pressed` | Cor de fundo no estado pressed | `var(--color-brand-01-light)` |
* | **Active (Selected)** | | |
* | `--background-color-actived` | Cor de fundo do item selecionado | `var(--color-brand-01-lightest)` |
* | `--color-actived` | Cor do texto do item selecionado | `var(--color-action-pressed)` |
*
*/
@Directive()
export class PoContextMenuBaseComponent {
/**
* Titulo do contexto superior
*/
contextTitle = input<string>('', { alias: 'p-context-title' });

/**
* Titulo principal do menu
*/
title = input<string>('', { alias: 'p-title' });

/**
* Lista de itens para renderizacao.
*
* > Ao receber os itens, o componente valida que apenas um item pode ter `selected: true`.
* > Se mais de um item estiver com `selected: true`, apenas o primeiro sera mantido como selecionado.
*/
items = input<Array<PoContextMenuItem>>([], { alias: 'p-items' });

/**
* Define se o menu estar aberto ou fechado.
*
* Suporta two-way binding:
*
* ```html
* <po-context-menu
* [(p-expanded)]="expanded"`
* />
* ```
*
* ou
*
* ```html
* <po-context-menu
* [(p-expanded)]="expanded"
* (p-expandChange)="handlerExpanded($event)
* />
* ```
*
* @default `true`
*/
expanded = model<boolean>(true, { alias: 'p-expanded' });

/**
* Evento emitido ao selecionar um item. Emite o item selecionado.
*/
itemSelected = output<PoContextMenuItem>({ alias: 'p-item-selected' });
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/**
* @usedBy PoContextMenuComponent
*
* @description
*
* Interface para os itens do componente po-context-menu.
*/
export interface PoContextMenuItem {
/** Texto do item de menu. */
label: string;

/** Acao executada ao clicar no item. */
action?: Function;

/** Estado de selecao do item. */
selected?: boolean;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<div class="po-context-menu" [class.po-context-menu-collapsed]="!expanded()">
<div class="po-context-menu-header">
<button
class="po-context-menu-toggle"
type="button"
[attr.aria-label]="expanded() ? 'Recolher menu' : 'Expandir menu'"
(click)="toggleExpand()"
>
<po-icon [p-icon]="expanded() ? 'ICON_SIDEBAR' : 'ICON_SIDEBAR_SIMPLES'"></po-icon>
</button>

@if (expanded()) {
<div class="po-context-menu-header-content">
@if (contextTitle()) {
<span class="po-context-menu-context-title">{{ contextTitle() }}</span>
}
@if (title()) {
<span class="po-context-menu-title">{{ title() }}</span>
}
</div>
}
</div>

@if (expanded()) {
<nav class="po-context-menu-body">
<ul class="po-context-menu-list" role="menu">
@for (item of _items(); track item.label) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Using item.label as both @for track key and selection identifier breaks with duplicate labels

The template at po-context-menu.component.html:27 uses track item.label in the @for loop, and selectItem at po-context-menu.component.ts:59 matches items by i.label === item.label. If a consumer provides two items with the same label (e.g., [{label: 'Settings', action: fn1}, {label: 'Settings', action: fn2}]), two things break: (1) Angular's @for requires unique track values and will produce incorrect DOM diffing or runtime errors with duplicate keys, and (2) selectItem will mark ALL items sharing that label as selected: true, violating the single-selection invariant the component enforces elsewhere via sanitizeSelection.

Prompt for agents
In projects/ui/src/lib/components/po-context-menu/po-context-menu.component.html line 27, change `track item.label` to `track $index` (or add a unique `id` field to the PoContextMenuItem interface and use `track item.id`). Additionally, in projects/ui/src/lib/components/po-context-menu/po-context-menu.component.ts line 59 inside `selectItem`, change the matching from label-based (`i.label === item.label`) to index-based or identity-based matching to avoid selecting multiple items with the same label.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

<li
#itemRef
class="po-context-menu-list-item"
role="menuitem"
tabindex="0"
[class.po-context-menu-item-selected]="item.selected"
(click)="selectItem(item)"
(keydown.enter)="selectItem(item)"
(keydown.space)="selectItem(item)"
(mouseenter)="handlerTooltip(item, itemRef)"
p-tooltip-position="right"
[p-tooltip]="item.tooltip"
>
<span class="po-context-menu-item-label">{{ item.label }}</span>
</li>
}
</ul>
</nav>
}
</div>
Loading
Loading