Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
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
74 changes: 72 additions & 2 deletions .amazonq/rules/eslint-plugin-development.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,8 @@ const iconChild = node.children?.find(

## Rule Implementation Pattern

### Single Component Rule

```typescript
import {
createAngularVisitors,
Expand Down Expand Up @@ -160,7 +162,8 @@ export default {
);
context.report({
loc,
messageId: MESSAGE_IDS.YOUR_MESSAGE_ID
messageId: MESSAGE_IDS.YOUR_MESSAGE_ID,
data: { component: node.name } // Use node.name for kebab-case
});
}
};
Expand All @@ -182,7 +185,11 @@ export default {
if (value === null || value === "") {
context.report({
node: openingElement,
messageId: MESSAGE_IDS.YOUR_MESSAGE_ID
messageId: MESSAGE_IDS.YOUR_MESSAGE_ID,
data: {
component:
openingElement.name?.name || openingElement.rawName
}
});
}
};
Expand All @@ -196,6 +203,64 @@ export default {
};
```

### Multiple Components Rule

**CRITICAL**: When checking multiple components, collect ALL Angular visitors before returning:

```typescript
const COMPONENTS_TO_CHECK = ["DBButton", "DBLink", "DBInput"];

export default {
meta: {
/* ... */
},
create(context: any) {
const angularHandler = (node: any, parserServices: any) => {
const component = COMPONENTS_TO_CHECK.find((comp) =>
isDBComponent(node, comp)
);
if (!component) return;

// Your validation logic here
};

// ❌ WRONG - Returns after first component, others never registered
for (const comp of COMPONENTS_TO_CHECK) {
const angularVisitors = createAngularVisitors(
context,
comp,
angularHandler
);
if (angularVisitors) return angularVisitors; // ❌ Early return!
}

// ✅ CORRECT - Collects all visitors before returning
const angularVisitors: any = {};
for (const comp of COMPONENTS_TO_CHECK) {
const visitors = createAngularVisitors(
context,
comp,
angularHandler
);
if (visitors) {
Object.assign(angularVisitors, visitors);
}
}
if (Object.keys(angularVisitors).length > 0) return angularVisitors;

const checkComponent = (node: any) => {
// React/Vue handler
};

return defineTemplateBodyVisitor(
context,
{ VElement: checkComponent, Element: checkComponent },
{ JSXElement: checkComponent }
);
}
};
```

## Adding New Messages

1. Add message to `MESSAGES` object in `src/shared/constants.ts`
Expand Down Expand Up @@ -257,10 +322,13 @@ invalid: [

- Angular boolean attributes return empty string `''` - handle with `attr.value === null || attr.value === ''`
- Use `createAngularVisitors` for Angular support - it handles kebab-case conversion for components starting with `DB`
- **CRITICAL**: When checking multiple components, collect ALL Angular visitors using `Object.assign()` before returning
- Always use `COMPONENTS` constants instead of hardcoded strings
- Always use `MESSAGE_IDS` and `MESSAGES` from constants
- For Angular, use `parserServices.convertNodeSourceSpanToLoc(node.sourceSpan)` for location
- For Angular, use `node.name` in error data to preserve kebab-case (e.g., `db-button`)
- For React/Vue, use `node: openingElement` for location
- For React/Vue, use `openingElement.name?.name || openingElement.rawName` for component name
- Angular template parser uses both `Element` and `Element$1` types
- Vue sometimes uses `Element` as fallback instead of `VElement`
- When traversing parents/children, always check for `JSXElement`, `VElement`, and `Element` types
Expand Down Expand Up @@ -335,6 +403,8 @@ Before submitting a new rule:
- [ ] Rule `fixable` uses `as const` assertion (if present)
- [ ] Test uses `languageOptions` configuration (not `parser` property)
- [ ] Angular support via `createAngularVisitors`
- [ ] **If checking multiple components**: Angular visitors collected with `Object.assign()` before returning
- [ ] Angular error data uses `node.name` for kebab-case component names
- [ ] Vue/React support via `defineTemplateBodyVisitor`
- [ ] All attribute checks use `=== null` (not `!value`)
- [ ] Required text attributes check `=== null || === ''`
Expand Down
5 changes: 5 additions & 0 deletions .changeset/fix-notification-closeable-eslint.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@db-ux/core-eslint-plugin": patch
---

fix(`DBNotification`): `close-button-text-required` rule now only requires `closeButtonText` when `closeable` attribute is set.
6 changes: 6 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -343,3 +343,9 @@ Or add it to your MCP client config:
### GitHub Actions / Pipelines

- Use `!cancelled()` instead of `always()` for controlling the step execution in GitHub Actions. This ensures that steps are skipped if the workflow run has been cancelled, preventing unnecessary execution and resource usage.

## Additional Resources

### ESLint Plugin

- Use this file as a reference for the custom ESLint plugin used in this repository: `.amazonq/rules/eslint-plugin-development.md`
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 6 additions & 6 deletions packages/components/docs/creating-custom-components.md
Original file line number Diff line number Diff line change
Expand Up @@ -628,12 +628,12 @@ export class CustomCardComponent {
3. **Set up the examples repo**: Create a new branch in the [examples repository](https://github.com/db-ux-design-system/examples) named after the component (e.g., `test-table`). Update the npm dependency for the framework you want to test with the pre-release version:
"Use the package matching your framework: @db-ux/react-core-components (React), @db-ux/ngx-core-components (Angular), @db-ux/v-core-components (Vue), @db-ux/wc-core-components (Web Components)."
`json
{
"dependencies": {
"@db-ux/react-core-components": "0.0.0-table-abc1234"
}
}
`
{
"dependencies": {
"@db-ux/react-core-components": "0.0.0-table-abc1234"
}
}
`

4. **Run user tests**: Users can either clone the examples repo and check out the branch, or open it directly in StackBlitz:

Expand Down
10 changes: 2 additions & 8 deletions packages/components/src/components/drawer/drawer.scss
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,7 @@
calc(100% - #{variables.$db-spacing-fixed-xl})
);
/* stylelint-disable-next-line db-ux/use-sizing */
min-block-size: var(
--db-drawer-min-height,
auto
);
min-block-size: var(--db-drawer-min-height, auto);
max-inline-size: none;
}
}
Expand Down Expand Up @@ -171,10 +168,7 @@ $spacings: (
calc(100% - #{variables.$db-spacing-fixed-xl})
);
/* stylelint-disable-next-line db-ux/use-sizing */
min-inline-size: var(
--db-drawer-min-width,
auto
);
min-inline-size: var(--db-drawer-min-width, auto);

&:not([data-direction]),
&[data-direction="right"] {
Expand Down
2 changes: 1 addition & 1 deletion packages/eslint-plugin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
"test:update": "vitest run --config vitest.config.ts --update"
},
"peerDependencies": {
"eslint": "^9.0.0 || ^10.0.0"
"eslint": ">=9.0.0"
},
"dependencies": {
"@angular-eslint/utils": "21.3.1",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,31 @@ export default {
},
create(context: any) {
const angularHandler = (node: any, parserServices: any) => {
const componentName = node.name;
const component = Object.keys(COMPONENTS_WITH_CLOSE_BUTTON).find(
(comp) => isDBComponent(node, comp)
);

if (!component) return;

if (component === 'DBNotification') {
const input = node.inputs?.find(
(i: any) => i.name === 'closeable'
);
// Check for [closeable]="false" - Angular AST structure
if (input) {
const val = input.value;
if (val?.type === 'LiteralPrimitive' && val.value === false)
return;
if (val?.source === 'false') return;
} else {
// Check for plain attribute closeable (no binding)
const attr = node.attributes?.find(
(a: any) => a.name === 'closeable'
);
if (!attr) return;
}
}

const attribute =
COMPONENTS_WITH_CLOSE_BUTTON[
component as keyof typeof COMPONENTS_WITH_CLOSE_BUTTON
Expand All @@ -47,19 +65,23 @@ export default {
context.report({
loc,
messageId: MESSAGE_IDS.CLOSE_BUTTON_TEXT_REQUIRED,
data: { component: componentName, attribute }
data: { component: node.name, attribute }
});
}
};

const angularVisitors: any = {};
for (const comp of Object.keys(COMPONENTS_WITH_CLOSE_BUTTON)) {
const angularVisitors = createAngularVisitors(
const visitors = createAngularVisitors(
context,
comp,
angularHandler
);
if (angularVisitors) return angularVisitors;
if (visitors) {
Object.assign(angularVisitors, visitors);
}
}
if (Object.keys(angularVisitors).length > 0) return angularVisitors;

const checkComponent = (node: any) => {
const openingElement = node.openingElement || node;
Expand All @@ -69,6 +91,60 @@ export default {

if (!component) return;

if (component === 'DBNotification') {
// React: closeable={false}
const closeableAttr = openingElement.attributes?.find(
(a: any) =>
a.type === 'JSXAttribute' && a.name.name === 'closeable'
);
if (
closeableAttr?.value?.type === 'JSXExpressionContainer' &&
closeableAttr.value.expression?.type === 'Literal' &&
closeableAttr.value.expression.value === false
)
return;

// Vue: key.name can be a VIdentifier object (key.name.name === 'bind')
// or a plain string for non-directive attributes
const isVueCloseableBind = (a: any) => {
const keyName =
typeof a.key?.name === 'string'
? a.key.name
: a.key?.name?.name;
return (
keyName === 'bind' &&
a.key?.argument?.name === 'closeable'
);
};
const isVueCloseableStatic = (a: any) => {
const keyName =
typeof a.key?.name === 'string'
? a.key.name
: a.key?.name?.name;
return keyName === 'closeable';
};

const vueBindAttr =
openingElement.startTag?.attributes?.find(
isVueCloseableBind
);
if (
vueBindAttr &&
(vueBindAttr.value?.value === 'false' ||
vueBindAttr.value?.expression?.value === false)
)
return;

// Only skip if closeable attribute/binding doesn't exist
const hasCloseable =
closeableAttr ||
openingElement.startTag?.attributes?.some(
(a: any) =>
isVueCloseableStatic(a) || isVueCloseableBind(a)
);
if (!hasCloseable) return;
}

const componentName =
openingElement.name?.name || openingElement.rawName;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,14 +58,18 @@ export default {
}
};

const angularVisitors: any = {};
for (const comp of COMPONENTS_REQUIRING_CONTENT) {
const angularVisitors = createAngularVisitors(
const visitors = createAngularVisitors(
context,
comp,
angularHandler
);
if (angularVisitors) return angularVisitors;
if (visitors) {
Object.assign(angularVisitors, visitors);
}
}
if (Object.keys(angularVisitors).length > 0) return angularVisitors;

const checkComponent = (node: any) => {
const openingElement = node.openingElement || node;
Expand Down
8 changes: 6 additions & 2 deletions packages/eslint-plugin/src/rules/form/form-label-required.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,14 +63,18 @@ export default {
}
};

const angularVisitors: any = {};
for (const comp of FORM_COMPONENTS) {
const angularVisitors = createAngularVisitors(
const visitors = createAngularVisitors(
context,
comp,
angularHandler
);
if (angularVisitors) return angularVisitors;
if (visitors) {
Object.assign(angularVisitors, visitors);
}
}
if (Object.keys(angularVisitors).length > 0) return angularVisitors;

const checkFormComponent = (node: any) => {
const openingElement = node.openingElement || node;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ export default {
);

if (iconChild) {
const iconValue = getAttributeValue(iconChild, 'icon');
const loc = parserServices.convertNodeSourceSpanToLoc(
iconChild.sourceSpan
);
Expand All @@ -62,14 +61,18 @@ export default {
}
};

const angularVisitors: any = {};
for (const comp of COMPONENTS_WITH_ICON_ATTR) {
const angularVisitors = createAngularVisitors(
const visitors = createAngularVisitors(
context,
comp,
angularHandler
);
if (angularVisitors) return angularVisitors;
if (visitors) {
Object.assign(angularVisitors, visitors);
}
}
if (Object.keys(angularVisitors).length > 0) return angularVisitors;

const checkComponent = (node: any) => {
const openingElement = node.openingElement || node;
Expand Down
Loading
Loading