From 71246164651bd425af5c396c17252b96bed76a17 Mon Sep 17 00:00:00 2001 From: Zaiidmo Date: Sun, 29 Mar 2026 12:41:33 +0100 Subject: [PATCH 01/14] initiated dev environment --- .github/workflows/pr-validation.yml | 41 +++++++++++++++++++++++++++++ package-lock.json | 8 +++--- package.json | 10 ++++--- 3 files changed, 52 insertions(+), 7 deletions(-) diff --git a/.github/workflows/pr-validation.yml b/.github/workflows/pr-validation.yml index e69de29..fc872ed 100644 --- a/.github/workflows/pr-validation.yml +++ b/.github/workflows/pr-validation.yml @@ -0,0 +1,41 @@ +name: CI - PR Validation + +on: + pull_request: + branches: [develop] + +permissions: + contents: read + +jobs: + validate: + name: CI - PR Validation + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + + - name: Install + run: npm ci + + - name: Format (check) + run: npm run format + + - name: Lint + run: npm run lint + + - name: Typecheck + run: npm run typecheck + + - name: Test + run: npm test + + - name: Build + run: npm run build diff --git a/package-lock.json b/package-lock.json index 30cfbb5..065e5b9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { - "name": "@ciscode/reactts-developerkit", - "version": "1.0.0", + "name": "@ciscode/ui-chart-kit", + "version": "0.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "@ciscode/reactts-developerkit", - "version": "1.0.0", + "name": "@ciscode/ui-chart-kit", + "version": "0.0.0", "license": "MIT", "devDependencies": { "@changesets/cli": "^2.27.8", diff --git a/package.json b/package.json index e1abcf6..2502981 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,14 @@ { - "name": "@ciscode/reactts-developerkit", - "version": "1.0.0", - "description": "React TypeScript hybrid library template (components + hooks + utils).", + "name": "@ciscode/ui-chart-kit", + "version": "0.0.0", + "description": "React 19 chart library on Chart.js.", "license": "MIT", "private": false, "type": "module", + "repository": { + "type": "git", + "url": "git+https://github.com/CISCODE-MA/ChartKit-UI.git" + }, "sideEffects": false, "files": [ "dist" From 29053dcfeb2f65c90d98be8fede5b2ebae529f34 Mon Sep 17 00:00:00 2001 From: Zaiidmo Date: Sun, 29 Mar 2026 12:47:42 +0100 Subject: [PATCH 02/14] ops: updated SOnar name --- .github/workflows/release-check.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release-check.yml b/.github/workflows/release-check.yml index 1ef2a3c..abd87c3 100644 --- a/.github/workflows/release-check.yml +++ b/.github/workflows/release-check.yml @@ -32,7 +32,7 @@ jobs: env: SONAR_HOST_URL: 'https://sonarcloud.io' SONAR_ORGANIZATION: 'ciscode' - SONAR_PROJECT_KEY: 'CISCODE-MA_PACKAGE_NAME_TEMPLATE' + SONAR_PROJECT_KEY: 'CISCODE-MA_ChartKit-UI' steps: - name: Checkout From 731aa0870dd4c6e3bb6ac07d5a396dd0ce274b25 Mon Sep 17 00:00:00 2001 From: Zaiidmo Date: Mon, 30 Mar 2026 10:40:43 +0100 Subject: [PATCH 03/14] ops (ci): standardize publish validation and dependabot across all packages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace git tag --list strategy with package.json-driven tag validation in all 16 publish workflows; use git rev-parse to verify the exact tag exists rather than guessing the latest repo-wide tag - Update error guidance to reflect feat/** → develop → master flow - Standardize dependabot to npm-only, grouped, monthly cadence across all 16 packages; remove github-actions ecosystem updates - Add missing dependabot.yml to AuthKit-UI, ChartKit-UI, HealthKit, HooksKit, paymentkit, StorageKit --- .github/dependabot.yml | 20 +++++++++++++ .github/workflows/pr-validation.yml | 2 +- .github/workflows/publish.yml | 44 +++++++++++++++++++++-------- .github/workflows/release-check.yml | 30 +++++--------------- 4 files changed, 61 insertions(+), 35 deletions(-) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..8d34dee --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,20 @@ +version: 2 +updates: + - package-ecosystem: npm + directory: '/' + schedule: + interval: monthly + open-pull-requests-limit: 1 + groups: + npm-dependencies: + patterns: + - '*' + assignees: + - CISCODE-MA/cloud-devops + labels: + - 'dependencies' + - 'npm' + commit-message: + prefix: 'chore(deps)' + include: 'scope' + rebase-strategy: auto diff --git a/.github/workflows/pr-validation.yml b/.github/workflows/pr-validation.yml index fc872ed..c8ac0d3 100644 --- a/.github/workflows/pr-validation.yml +++ b/.github/workflows/pr-validation.yml @@ -19,7 +19,7 @@ jobs: - name: Setup Node uses: actions/setup-node@v4 with: - node-version: 20 + node-version: 22 cache: npm - name: Install diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 8465462..ffe4408 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -20,30 +20,52 @@ jobs: with: fetch-depth: 0 - - name: Validate tag exists on this push + - name: Validate version tag and package.json run: | - TAG=$(git describe --exact-match --tags HEAD 2>/dev/null || echo "") - if [[ -z "$TAG" ]]; then - echo "❌ No tag found on HEAD. This push did not include a version tag." - echo "To publish, merge to master with a tag: git tag v1.0.0 && git push origin master --tags" + PKG_VERSION=$(grep '"version"' package.json | head -1 | sed 's/.*"version": "\([^"]*\)".*/\1/') + TAG="v${PKG_VERSION}" + + if [[ -z "$PKG_VERSION" ]]; then + echo "❌ ERROR: Could not read version from package.json" exit 1 fi + if [[ ! "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - echo "❌ Invalid tag format: $TAG. Expected: v*.*.*" + echo "❌ ERROR: Invalid version format in package.json: '$PKG_VERSION'" + echo "Expected format: x.y.z (e.g., 1.0.0, 0.2.3)" + exit 1 + fi + + if ! git rev-parse "$TAG" >/dev/null 2>&1; then + echo "❌ ERROR: Tag $TAG not found!" + echo "" + echo "This typically happens when:" + echo " 1. You forgot to run 'npm version patch|minor|major' on your feature branch" + echo " 2. You didn't push the tag: git push origin --tags" + echo " 3. The tag was created locally but never pushed to remote" + echo "" + echo "📋 Correct workflow:" + echo " 1. On feat/** or feature/**: npm version patch (or minor/major)" + echo " 2. Push branch + tag: git push origin feat/your-feature --tags" + echo " 3. PR feat/** → develop, then PR develop → master" + echo " 4. Workflow automatically triggers on master push" + echo "" exit 1 fi - echo "✅ Valid tag found: $TAG" + + echo "✅ package.json version: $PKG_VERSION" + echo "✅ Tag $TAG exists in repo" echo "TAG_VERSION=$TAG" >> $GITHUB_ENV - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: '20' + node-version: '22' registry-url: 'https://registry.npmjs.org' - cache: npm + cache: 'npm' - name: Install dependencies - run: npm install + run: npm ci - name: Build run: npm run build --if-present @@ -55,6 +77,6 @@ jobs: run: npm test --if-present 2>/dev/null || true - name: Publish to NPM - run: npm publish --access public --no-git-checks + run: npm publish --access public --provenance env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.github/workflows/release-check.yml b/.github/workflows/release-check.yml index abd87c3..a5ecb64 100644 --- a/.github/workflows/release-check.yml +++ b/.github/workflows/release-check.yml @@ -3,16 +3,6 @@ name: CI - Release Check on: pull_request: branches: [master] - workflow_dispatch: - inputs: - sonar: - description: 'Run SonarCloud analysis' - required: true - default: 'false' - type: choice - options: - - 'false' - - 'true' concurrency: group: ci-release-${{ github.ref }} @@ -27,8 +17,6 @@ jobs: permissions: contents: read - # Update these values for your package: - # - SONAR_PROJECT_KEY: "CISCODE-MA_YourPackageName" env: SONAR_HOST_URL: 'https://sonarcloud.io' SONAR_ORGANIZATION: 'ciscode' @@ -43,14 +31,11 @@ jobs: - name: Setup Node uses: actions/setup-node@v4 with: - node-version: '20' - cache: npm + node-version: '22' + cache: 'npm' - name: Install - run: npm install - - - name: Audit - run: npm audit --prod + run: npm ci - name: Format run: npm run format @@ -68,20 +53,19 @@ jobs: run: npm run build - name: SonarCloud Scan - if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.sonar == 'true' }} uses: SonarSource/sonarqube-scan-action@v6 env: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} SONAR_HOST_URL: ${{ env.SONAR_HOST_URL }} with: args: > - -Dsonar.organization=${{ env.SONAR_ORGANIZATION }} \ - -Dsonar.projectKey=${{ env.SONAR_PROJECT_KEY }} \ - -Dsonar.sources=src \ + -Dsonar.organization=${{ env.SONAR_ORGANIZATION }} + -Dsonar.projectKey=${{ env.SONAR_PROJECT_KEY }} + -Dsonar.sources=src + -Dsonar.tests=test -Dsonar.javascript.lcov.reportPaths=coverage/lcov.info - name: SonarCloud Quality Gate - if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.sonar == 'true' }} uses: SonarSource/sonarqube-quality-gate-action@v1 timeout-minutes: 10 env: From 50134efc45348af8cc3d2a0096ab0d34ce619c42 Mon Sep 17 00:00:00 2001 From: Zaiidmo Date: Mon, 30 Mar 2026 16:43:32 +0100 Subject: [PATCH 04/14] security: added CODEOWNER file for branches security \\ --- .github/CODEOWNERS | 1 + 1 file changed, 1 insertion(+) create mode 100644 .github/CODEOWNERS diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..2279f0b --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @CISCODE-MA/devops From f94c084ed17e6ec78b63ccb4e5f8257bf9d4fb8a Mon Sep 17 00:00:00 2001 From: Zaiidmo Date: Mon, 30 Mar 2026 16:43:45 +0100 Subject: [PATCH 05/14] ops: updated release check workflow --- .github/workflows/release-check.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release-check.yml b/.github/workflows/release-check.yml index a5ecb64..5cd4b42 100644 --- a/.github/workflows/release-check.yml +++ b/.github/workflows/release-check.yml @@ -62,7 +62,8 @@ jobs: -Dsonar.organization=${{ env.SONAR_ORGANIZATION }} -Dsonar.projectKey=${{ env.SONAR_PROJECT_KEY }} -Dsonar.sources=src - -Dsonar.tests=test + -Dsonar.tests=src/__tests__ + -Dsonar.exclusions=src/__tests__/** -Dsonar.javascript.lcov.reportPaths=coverage/lcov.info - name: SonarCloud Quality Gate From 5c215fc8ebe2d2601bc7f15d02d52ff4d7ddea28 Mon Sep 17 00:00:00 2001 From: Zaiidmo Date: Tue, 31 Mar 2026 10:00:16 +0100 Subject: [PATCH 06/14] ops: updated relese check workflow# --- .github/workflows/release-check.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.github/workflows/release-check.yml b/.github/workflows/release-check.yml index 5cd4b42..f330be6 100644 --- a/.github/workflows/release-check.yml +++ b/.github/workflows/release-check.yml @@ -16,6 +16,7 @@ jobs: permissions: contents: read + statuses: write env: SONAR_HOST_URL: 'https://sonarcloud.io' @@ -72,3 +73,16 @@ jobs: env: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} SONAR_HOST_URL: ${{ env.SONAR_HOST_URL }} + + - name: Report CI status + if: always() + uses: actions/github-script@v7 + with: + script: | + await github.rest.repos.createCommitStatus({ + owner: context.repo.owner, + repo: context.repo.repo, + sha: context.sha, + state: '${{ job.status }}' === 'success' ? 'success' : 'failure', + description: 'CI checks completed' + }) From 189c71ea5c5de2241439b0cb817592e4acaf7350 Mon Sep 17 00:00:00 2001 From: Zaiidmo Date: Mon, 6 Apr 2026 09:04:52 +0100 Subject: [PATCH 07/14] ci: update release check workflow --- .github/workflows/release-check.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/release-check.yml b/.github/workflows/release-check.yml index f330be6..53792e7 100644 --- a/.github/workflows/release-check.yml +++ b/.github/workflows/release-check.yml @@ -10,7 +10,6 @@ concurrency: jobs: ci: - name: release checks runs-on: ubuntu-latest timeout-minutes: 25 From 92f099ef5a6bc4019c7be0c5deaab28400601582 Mon Sep 17 00:00:00 2001 From: Omaima33 Date: Mon, 6 Apr 2026 16:10:17 +0100 Subject: [PATCH 08/14] Refactor (#2) * feat: add typed chart contracts and buildChartConfig utility with bar/line/area support and theme-based color fallback * feat: add BarChart component with typed props, stacking/horizontal support, and internal config builder * feat: add LineChart and AreaChart with smooth option, area fill, stacking, and shared config builder * test: add full test suite with mocked react-chartjs-2, config validation, and 80%+ coverage * chore: add README and changeset * fix: extract shared test fixtures to resolve SonarCloud duplication --- .changeset/initial-release.md | 12 ++ .vscode/mcp.json | 8 + README.md | 192 ++++++++++++++++---- ciscode-reactts-developerkit-1.0.0.tgz | Bin 0 -> 8656 bytes ciscode-ui-chart-kit-0.1.0.tgz | Bin 0 -> 9154 bytes package-lock.json | 66 +++++-- package.json | 8 +- src/__tests__/chart-test-utils.ts | 113 ++++++++++++ src/components/AreaChart/AreaChart.test.tsx | 95 ++++++++++ src/components/AreaChart/AreaChart.tsx | 61 +++++++ src/components/AreaChart/AreaChart.types.ts | 14 ++ src/components/AreaChart/index.ts | 2 + src/components/BarChart/BarChart.test.tsx | 93 ++++++++++ src/components/BarChart/BarChart.tsx | 57 ++++++ src/components/BarChart/BarChart.types.ts | 14 ++ src/components/BarChart/index.ts | 2 + src/components/LineChart/LineChart.test.tsx | 59 ++++++ src/components/LineChart/LineChart.tsx | 55 ++++++ src/components/LineChart/LineChart.types.ts | 12 ++ src/components/LineChart/index.ts | 2 + src/components/index.ts | 3 + src/index.ts | 1 + src/types/chart.types.ts | 70 +++++++ src/types/index.ts | 1 + src/utils/buildChartConfig.test.ts | 168 +++++++++++++++++ src/utils/buildChartConfig.ts | 88 +++++++++ src/utils/index.ts | 1 + 27 files changed, 1151 insertions(+), 46 deletions(-) create mode 100644 .changeset/initial-release.md create mode 100644 .vscode/mcp.json create mode 100644 ciscode-reactts-developerkit-1.0.0.tgz create mode 100644 ciscode-ui-chart-kit-0.1.0.tgz create mode 100644 src/__tests__/chart-test-utils.ts create mode 100644 src/components/AreaChart/AreaChart.test.tsx create mode 100644 src/components/AreaChart/AreaChart.tsx create mode 100644 src/components/AreaChart/AreaChart.types.ts create mode 100644 src/components/AreaChart/index.ts create mode 100644 src/components/BarChart/BarChart.test.tsx create mode 100644 src/components/BarChart/BarChart.tsx create mode 100644 src/components/BarChart/BarChart.types.ts create mode 100644 src/components/BarChart/index.ts create mode 100644 src/components/LineChart/LineChart.test.tsx create mode 100644 src/components/LineChart/LineChart.tsx create mode 100644 src/components/LineChart/LineChart.types.ts create mode 100644 src/components/LineChart/index.ts create mode 100644 src/types/chart.types.ts create mode 100644 src/types/index.ts create mode 100644 src/utils/buildChartConfig.test.ts create mode 100644 src/utils/buildChartConfig.ts diff --git a/.changeset/initial-release.md b/.changeset/initial-release.md new file mode 100644 index 0000000..bae87b5 --- /dev/null +++ b/.changeset/initial-release.md @@ -0,0 +1,12 @@ +--- +'@ciscode/ui-chart-kit': minor +--- + +Initial release of @ciscode/ui-chart-kit v0.1.0. + +- ChartDataPoint, ChartDataset, and ChartTheme type contracts +- buildChartConfig utility mapping typed data to Chart.js config +- BarChart component with stacked and horizontal support +- LineChart component with smooth curve support +- AreaChart component with fill at 20% opacity and stacked support +- All components responsive with configurable height diff --git a/.vscode/mcp.json b/.vscode/mcp.json new file mode 100644 index 0000000..93ca411 --- /dev/null +++ b/.vscode/mcp.json @@ -0,0 +1,8 @@ +{ + "servers": { + "github": { + "command": "npx", + "args": ["-y", "@modelcontextprotocol/server-github"] + } + } +} diff --git a/README.md b/README.md index 539fe85..d2fcecb 100644 --- a/README.md +++ b/README.md @@ -1,44 +1,170 @@ -# React TypeScript DeveloperKit (Template) +# @ciscode/ui-chart-kit -Template repository for building reusable React TypeScript **npm libraries** -(components + hooks + utilities). +Typed React chart components (Bar, Line, Area) built on Chart.js. +Pass data and a theme — get a fully configured, responsive chart. No raw Chart.js options required. -## What you get +## Installation -- ESM + CJS + Types build (tsup) -- Vitest testing -- ESLint + Prettier (flat config) -- Changesets (manual release flow, no automation PR) -- Husky (pre-commit + pre-push) -- Enforced public API via `src/index.ts` -- Dependency-free styling (Tailwind-compatible by convention only) -- `react` and `react-dom` as peerDependencies +```bash +npm install @ciscode/ui-chart-kit +``` -## Package structure +### Peer dependencies -- `src/components` – reusable UI components -- `src/hooks` – reusable React hooks -- `src/utils` – framework-agnostic utilities -- `src/index.ts` – **only public API** (no deep imports allowed) +| Package | Version | +| ----------- | ------- | +| `react` | ≥ 18 | +| `react-dom` | ≥ 18 | -Anything not exported from `src/index.ts` is considered private. +`chart.js` and `react-chartjs-2` are bundled — you do **not** need to install them separately. -## Scripts +--- -- `npm run build` – build to `dist/` (tsup) -- `npm test` – run tests (vitest) -- `npm run typecheck` – TypeScript typecheck -- `npm run lint` – ESLint -- `npm run format` / `npm run format:write` – Prettier -- `npx changeset` – create a changeset +## Data types -## Release flow (summary) +### `ChartDataPoint` -- Work on a `feature` branch from `develop` -- Merge to `develop` -- Add a changeset for user-facing changes: `npx changeset` -- Promote `develop` → `master` -- Tag `vX.Y.Z` to publish (npm OIDC) +```ts +interface ChartDataPoint { + label: string; + value: number; +} +``` -This repository is a **template**. Teams should clone it and focus only on -library logic, not tooling or release mechanics. +### `ChartDataset` + +```ts +interface ChartDataset { + id: string; + label: string; + data: ChartDataPoint[]; + color?: string; // hex color — falls back to theme.colors when omitted +} +``` + +### `ChartTheme` + +```ts +interface ChartTheme { + colors: string[]; // palette shared across datasets + fontFamily?: string; + fontSize?: number; + grid?: { + color?: string; + display?: boolean; + }; + tooltip?: { + enabled?: boolean; + backgroundColor?: string; + titleColor?: string; + bodyColor?: string; + }; + legend?: { + display?: boolean; + position?: 'top' | 'bottom' | 'left' | 'right'; + }; +} +``` + +--- + +## Components + +### BarChart + +| Prop | Type | Default | Description | +| ------------ | ---------------- | ------- | --------------------------------- | +| `data` | `ChartDataset[]` | — | Datasets to render | +| `theme` | `ChartTheme` | — | Theme (colors, fonts, grid, etc.) | +| `height` | `number` | `300` | Chart height in pixels | +| `stacked` | `boolean` | `false` | Stack bars on top of each other | +| `horizontal` | `boolean` | `false` | Render horizontal bars | + +```tsx +import { BarChart } from '@ciscode/ui-chart-kit'; +import type { ChartDataset, ChartTheme } from '@ciscode/ui-chart-kit'; + +const theme: ChartTheme = { + colors: ['#4F46E5', '#10B981', '#F59E0B'], + fontFamily: 'Inter, sans-serif', + fontSize: 12, + grid: { color: '#E5E7EB', display: true }, + tooltip: { enabled: true, backgroundColor: '#1F2937' }, + legend: { display: true, position: 'top' }, +}; + +const datasets: ChartDataset[] = [ + { + id: 'revenue', + label: 'Revenue', + data: [ + { label: 'Q1', value: 120 }, + { label: 'Q2', value: 180 }, + { label: 'Q3', value: 150 }, + { label: 'Q4', value: 210 }, + ], + }, +]; + +function App() { + return ; +} +``` + +--- + +### LineChart + +| Prop | Type | Default | Description | +| -------- | ---------------- | ------- | --------------------------------------- | +| `data` | `ChartDataset[]` | — | Datasets to render | +| `theme` | `ChartTheme` | — | Theme (colors, fonts, grid, etc.) | +| `height` | `number` | `300` | Chart height in pixels | +| `smooth` | `boolean` | `false` | Curved line interpolation (0.4 tension) | + +```tsx +import { LineChart } from '@ciscode/ui-chart-kit'; + +function App() { + return ; +} +``` + +--- + +### AreaChart + +| Prop | Type | Default | Description | +| --------- | ---------------- | ------- | --------------------------------------- | +| `data` | `ChartDataset[]` | — | Datasets to render | +| `theme` | `ChartTheme` | — | Theme (colors, fonts, grid, etc.) | +| `height` | `number` | `300` | Chart height in pixels | +| `smooth` | `boolean` | `false` | Curved line interpolation (0.4 tension) | +| `stacked` | `boolean` | `false` | Stack areas on top of each other | + +Area fill uses the dataset color at 20 % opacity automatically. + +```tsx +import { AreaChart } from '@ciscode/ui-chart-kit'; + +function App() { + return ; +} +``` + +--- + +## Design decisions + +- **No Chart.js passthrough.** Components expose a curated props API only. + Chart.js configuration is built internally via `buildChartConfig`. + This keeps the public surface small and prevents breaking changes + when Chart.js internals evolve. +- **Colors cycle.** When there are more datasets than `theme.colors` entries, + colors wrap around automatically. +- **Responsive by default.** Every chart renders inside a `div` with + `width: 100%` and the specified `height`. + +## License + +MIT diff --git a/ciscode-reactts-developerkit-1.0.0.tgz b/ciscode-reactts-developerkit-1.0.0.tgz new file mode 100644 index 0000000000000000000000000000000000000000..47778b68af3a669565aec3164fa6bed25962f72c GIT binary patch literal 8656 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 R&ku0K@4)~*ge?Sw{{zl2>CONE literal 0 HcmV?d00001 diff --git a/ciscode-ui-chart-kit-0.1.0.tgz b/ciscode-ui-chart-kit-0.1.0.tgz new file mode 100644 index 0000000000000000000000000000000000000000..36feeed25c9e87c5c844271582cdd63fc774883a GIT binary patch literal 9154 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 EUx`b>_5c6? literal 0 HcmV?d00001 diff --git a/package-lock.json b/package-lock.json index 065e5b9..1599bb6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,10 @@ "name": "@ciscode/ui-chart-kit", "version": "0.0.0", "license": "MIT", + "dependencies": { + "chart.js": "^4.5.1", + "react-chartjs-2": "^5.3.1" + }, "devDependencies": { "@changesets/cli": "^2.27.8", "@eslint/js": "^9.39.2", @@ -158,6 +162,7 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -779,6 +784,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=20.19.0" }, @@ -819,6 +825,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=20.19.0" } @@ -1736,6 +1743,12 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", + "license": "MIT" + }, "node_modules/@manypkg/find-root": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@manypkg/find-root/-/find-root-1.1.0.tgz", @@ -2309,8 +2322,7 @@ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@types/estree": { "version": "1.0.8", @@ -2339,6 +2351,7 @@ "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" @@ -2350,6 +2363,7 @@ "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", "dev": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^18.0.0" } @@ -2389,6 +2403,7 @@ "integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/types": "8.56.1", @@ -2724,6 +2739,7 @@ "integrity": "sha512-izzd2zmnk8Nl5ECYkW27328RbQ1nKvkm6Bb5DAaz1Gk59EbLkiCMa6OLT0NoaAYTjOFS6N+SMYW1nh4/9ljPiw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vitest/utils": "2.1.9", "fflate": "^0.8.2", @@ -2761,6 +2777,7 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3157,6 +3174,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -3319,6 +3337,19 @@ "dev": true, "license": "MIT" }, + "node_modules/chart.js": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz", + "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, "node_modules/check-error": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", @@ -3712,8 +3743,7 @@ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/dunder-proto": { "version": "1.0.1", @@ -3982,6 +4012,7 @@ "dev": true, "hasInstallScript": true, "license": "MIT", + "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -4046,6 +4077,7 @@ "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -5838,6 +5870,7 @@ "integrity": "sha512-0+MoQNYyr2rBHqO1xilltfDjV9G7ymYGlAUazgcDLQaUf8JDHbuGwsxN6U9qWaElZ4w1B2r7yEGIL3GdeW3Rug==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@acemir/cssom": "^0.9.31", "@asamuzakjp/dom-selector": "^6.8.1", @@ -6217,7 +6250,6 @@ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, "license": "MIT", - "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -6975,6 +7007,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -7059,7 +7092,6 @@ "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -7075,7 +7107,6 @@ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=10" }, @@ -7160,6 +7191,16 @@ "node": ">=0.10.0" } }, + "node_modules/react-chartjs-2": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.3.1.tgz", + "integrity": "sha512-h5IPXKg9EXpjoBzUfyWJvllMjG2mQ4EiuHQFhms/AjUm0XSZHhyRy2xVmLXHKrtcdrPO4mnGqRtYoD0vp95A0A==", + "license": "MIT", + "peerDependencies": { + "chart.js": "^4.1.1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/react-dom": { "version": "19.2.4", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", @@ -7178,8 +7219,7 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/read-yaml-file": { "version": "1.1.0", @@ -7556,8 +7596,7 @@ "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/semver": { "version": "7.7.4", @@ -8376,6 +8415,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -8662,6 +8702,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -8787,6 +8828,7 @@ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", @@ -9300,6 +9342,7 @@ "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vitest/expect": "2.1.9", "@vitest/mocker": "2.1.9", @@ -9710,6 +9753,7 @@ "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "dev": true, "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/package.json b/package.json index 2502981..7ef117a 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@ciscode/ui-chart-kit", - "version": "0.0.0", - "description": "React 19 chart library on Chart.js.", + "version": "0.1.0", + "description": "Typed React chart components (Bar, Line, Area) built on Chart.js — no raw Chart.js config required.", "license": "MIT", "private": false, "type": "module", @@ -84,5 +84,9 @@ }, "engines": { "node": ">=20" + }, + "dependencies": { + "chart.js": "^4.5.1", + "react-chartjs-2": "^5.3.1" } } diff --git a/src/__tests__/chart-test-utils.ts b/src/__tests__/chart-test-utils.ts new file mode 100644 index 0000000..0e17d99 --- /dev/null +++ b/src/__tests__/chart-test-utils.ts @@ -0,0 +1,113 @@ +import { render, screen } from '@testing-library/react'; +import { expect, it } from 'vitest'; +import React from 'react'; +import type { ChartDataset, ChartTheme } from '../types/chart.types'; + +export const singleDataset: ChartDataset[] = [ + { + id: 'ds1', + label: 'Sales', + data: [ + { label: 'Jan', value: 10 }, + { label: 'Feb', value: 20 }, + ], + }, +]; + +export const multiDatasets: ChartDataset[] = [ + { + id: 'ds1', + label: 'Sales', + data: [ + { label: 'Jan', value: 10 }, + { label: 'Feb', value: 20 }, + ], + }, + { + id: 'ds2', + label: 'Revenue', + data: [ + { label: 'Jan', value: 30 }, + { label: 'Feb', value: 40 }, + ], + color: '#00FF00', + }, + { + id: 'ds3', + label: 'Costs', + data: [ + { label: 'Jan', value: 5 }, + { label: 'Feb', value: 15 }, + ], + }, +]; + +export const defaultTheme: ChartTheme = { + colors: ['#FF0000', '#00FF00', '#0000FF'], + fontFamily: 'Arial', + fontSize: 14, + grid: { color: '#ccc', display: true }, + tooltip: { enabled: true, backgroundColor: '#333' }, + legend: { display: true, position: 'bottom' }, +}; + +export function getCanvasData(testId: string) { + const canvas = screen.getByTestId(testId); + return { + canvas, + data: JSON.parse(canvas.getAttribute('data-data')!), + options: JSON.parse(canvas.getAttribute('data-options')!), + }; +} + +export function describeCommonChartBehavior( + ChartComponent: React.ComponentType<{ data: ChartDataset[]; theme: ChartTheme; height?: number }>, + canvasTestId: string, +) { + it('should render with 1 dataset without errors', () => { + render(React.createElement(ChartComponent, { data: singleDataset, theme: defaultTheme })); + expect(screen.getByTestId(canvasTestId)).toBeInTheDocument(); + }); + + it('should render with multiple datasets without errors', () => { + render(React.createElement(ChartComponent, { data: multiDatasets, theme: defaultTheme })); + const { data } = getCanvasData(canvasTestId); + expect(data.datasets).toHaveLength(3); + }); + + it('should wrap chart in div with width 100% and default height 300', () => { + const { container } = render( + React.createElement(ChartComponent, { data: singleDataset, theme: defaultTheme }), + ); + const wrapper = container.firstElementChild as HTMLDivElement; + expect(wrapper.style.width).toBe('100%'); + expect(wrapper.style.height).toBe('300px'); + }); + + it('should apply custom height', () => { + const { container } = render( + React.createElement(ChartComponent, { + data: singleDataset, + theme: defaultTheme, + height: 500, + }), + ); + const wrapper = container.firstElementChild as HTMLDivElement; + expect(wrapper.style.height).toBe('500px'); + }); + + it('should reflect theme settings in options', () => { + render(React.createElement(ChartComponent, { data: singleDataset, theme: defaultTheme })); + const { options } = getCanvasData(canvasTestId); + expect(options.plugins.tooltip.enabled).toBe(true); + expect(options.plugins.legend.position).toBe('bottom'); + expect(options.responsive).toBe(true); + }); + + it('should handle empty data array without crash', () => { + render(React.createElement(ChartComponent, { data: [], theme: defaultTheme })); + const { data } = getCanvasData(canvasTestId); + expect(data.labels).toEqual([]); + expect(data.datasets).toEqual([]); + }); +} diff --git a/src/components/AreaChart/AreaChart.test.tsx b/src/components/AreaChart/AreaChart.test.tsx new file mode 100644 index 0000000..75274ca --- /dev/null +++ b/src/components/AreaChart/AreaChart.test.tsx @@ -0,0 +1,95 @@ +import { describe, it, expect, vi } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import React from 'react'; +import { + singleDataset, + multiDatasets, + defaultTheme, + getCanvasData, + describeCommonChartBehavior, +} from '../../__tests__/chart-test-utils'; + +vi.mock('react-chartjs-2', () => ({ + Line: (props: { data: unknown; options: unknown }) => ( + + ), +})); + +vi.mock('chart.js', () => ({ + Chart: { register: vi.fn() }, + CategoryScale: 'CategoryScale', + LinearScale: 'LinearScale', + LineElement: 'LineElement', + PointElement: 'PointElement', + Filler: 'Filler', + Tooltip: 'Tooltip', + Legend: 'Legend', +})); + +import { AreaChart } from './AreaChart'; + +const CANVAS_TEST_ID = 'area-canvas'; + +describe('AreaChart', () => { + describeCommonChartBehavior(AreaChart, CANVAS_TEST_ID); + + it('should set fill true on all datasets (area variant)', () => { + render(); + const { data } = getCanvasData(CANVAS_TEST_ID); + for (const ds of data.datasets) { + expect(ds.fill).toBe(true); + } + }); + + it('should apply 20% opacity (append 33) to backgroundColor', () => { + render(); + const { data } = getCanvasData(CANVAS_TEST_ID); + expect(data.datasets[0].backgroundColor).toBe('#FF000033'); + }); + + it('should use dataset color with 20% opacity when color is provided', () => { + render(); + const { data } = getCanvasData(CANVAS_TEST_ID); + expect(data.datasets[1].backgroundColor).toBe('#00FF0033'); + }); + + it('should set tension 0.4 on all datasets when smooth is true', () => { + render(); + const { data } = getCanvasData(CANVAS_TEST_ID); + for (const ds of data.datasets) { + expect(ds.tension).toBe(0.4); + } + }); + + it('should not set tension when smooth is false', () => { + render(); + const { data } = getCanvasData(CANVAS_TEST_ID); + expect(data.datasets[0].tension).toBeUndefined(); + }); + + it('should set scales.y.stacked true when stacked is true', () => { + render(); + const { options } = getCanvasData(CANVAS_TEST_ID); + expect(options.scales.y.stacked).toBe(true); + }); + + it('should not set stacked when stacked prop is false', () => { + render(); + const { options } = getCanvasData(CANVAS_TEST_ID); + expect(options.scales.y.stacked).toBeUndefined(); + }); + + it('should support stacked and smooth together', () => { + render(); + const { options, data } = getCanvasData(CANVAS_TEST_ID); + expect(options.scales.y.stacked).toBe(true); + for (const ds of data.datasets) { + expect(ds.tension).toBe(0.4); + expect(ds.fill).toBe(true); + } + }); +}); diff --git a/src/components/AreaChart/AreaChart.tsx b/src/components/AreaChart/AreaChart.tsx new file mode 100644 index 0000000..74c2741 --- /dev/null +++ b/src/components/AreaChart/AreaChart.tsx @@ -0,0 +1,61 @@ +import React, { useMemo } from 'react'; +import { + Chart as ChartJS, + CategoryScale, + LinearScale, + LineElement, + PointElement, + Filler, + Tooltip, + Legend, +} from 'chart.js'; +import { Line } from 'react-chartjs-2'; +import { buildChartConfig } from '../../utils/buildChartConfig'; +import type { AreaChartProps } from './AreaChart.types'; + +ChartJS.register(CategoryScale, LinearScale, LineElement, PointElement, Filler, Tooltip, Legend); + +/** + * AreaChart renders a filled line chart using react-chartjs-2. + * Uses the 'area' variant from buildChartConfig for fill with 20% opacity. + * Supports smooth curves and stacked mode. + * + * @example + * ```tsx + * + * ``` + */ +export const AreaChart: React.FC = ({ + data, + theme, + height = 300, + smooth = false, + stacked = false, +}) => { + const config = useMemo(() => { + const base = buildChartConfig(data, theme, 'area'); + + if (smooth) { + for (const ds of base.data.datasets) { + ds.tension = 0.4; + } + } + + if (stacked) { + base.options.scales.y.stacked = true; + } + + return base; + }, [data, theme, smooth, stacked]); + + return ( +
+ ['options']} + /> +
+ ); +}; + +AreaChart.displayName = 'AreaChart'; diff --git a/src/components/AreaChart/AreaChart.types.ts b/src/components/AreaChart/AreaChart.types.ts new file mode 100644 index 0000000..096f8b0 --- /dev/null +++ b/src/components/AreaChart/AreaChart.types.ts @@ -0,0 +1,14 @@ +import type { ChartDataset, ChartTheme } from '../../types/chart.types'; + +export interface AreaChartProps { + /** Array of datasets to render */ + data: ChartDataset[]; + /** Theme configuration for colors, fonts, grid, tooltip, and legend */ + theme: ChartTheme; + /** Chart height in pixels @default 300 */ + height?: number; + /** Apply curved line interpolation (tension 0.4) @default false */ + smooth?: boolean; + /** Stack area datasets on top of each other */ + stacked?: boolean; +} diff --git a/src/components/AreaChart/index.ts b/src/components/AreaChart/index.ts new file mode 100644 index 0000000..75803cd --- /dev/null +++ b/src/components/AreaChart/index.ts @@ -0,0 +1,2 @@ +export { AreaChart } from './AreaChart'; +export type { AreaChartProps } from './AreaChart.types'; diff --git a/src/components/BarChart/BarChart.test.tsx b/src/components/BarChart/BarChart.test.tsx new file mode 100644 index 0000000..da80a97 --- /dev/null +++ b/src/components/BarChart/BarChart.test.tsx @@ -0,0 +1,93 @@ +import { describe, it, expect, vi } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import React from 'react'; +import { + singleDataset, + multiDatasets, + defaultTheme, + getCanvasData, + describeCommonChartBehavior, +} from '../../__tests__/chart-test-utils'; + +vi.mock('react-chartjs-2', () => ({ + Bar: (props: { data: unknown; options: unknown }) => ( + + ), +})); + +vi.mock('chart.js', () => ({ + Chart: { register: vi.fn() }, + CategoryScale: 'CategoryScale', + LinearScale: 'LinearScale', + BarElement: 'BarElement', + Tooltip: 'Tooltip', + Legend: 'Legend', +})); + +import { BarChart } from './BarChart'; + +const CANVAS_TEST_ID = 'bar-canvas'; + +describe('BarChart', () => { + describeCommonChartBehavior(BarChart, CANVAS_TEST_ID); + + it('should set stacked on both axes when stacked prop is true', () => { + render(); + const { options } = getCanvasData(CANVAS_TEST_ID); + expect(options.scales.x.stacked).toBe(true); + expect(options.scales.y.stacked).toBe(true); + }); + + it('should not set stacked when stacked prop is false', () => { + render(); + const { options } = getCanvasData(CANVAS_TEST_ID); + expect(options.scales.x.stacked).toBeUndefined(); + expect(options.scales.y.stacked).toBeUndefined(); + }); + + it('should set indexAxis to y when horizontal is true', () => { + render(); + const { options } = getCanvasData(CANVAS_TEST_ID); + expect(options.indexAxis).toBe('y'); + }); + + it('should not set indexAxis when horizontal is false', () => { + render(); + const { options } = getCanvasData(CANVAS_TEST_ID); + expect(options.indexAxis).toBeUndefined(); + }); + + it('should pass animation enabled by default (no explicit disable)', () => { + render(); + const { options } = getCanvasData(CANVAS_TEST_ID); + expect(options.animation).toBeUndefined(); + }); + + it('should reflect theme tooltip and legend settings in options', () => { + render(); + const { options } = getCanvasData(CANVAS_TEST_ID); + expect(options.plugins.tooltip.enabled).toBe(true); + expect(options.plugins.tooltip.backgroundColor).toBe('#333'); + expect(options.plugins.legend.display).toBe(true); + expect(options.plugins.legend.position).toBe('bottom'); + }); + + it('should support stacked and horizontal together', () => { + render(); + const { options } = getCanvasData(CANVAS_TEST_ID); + expect(options.scales.x.stacked).toBe(true); + expect(options.scales.y.stacked).toBe(true); + expect(options.indexAxis).toBe('y'); + }); + + it('should handle empty data array without crash', () => { + render(); + const { data } = getCanvasData(CANVAS_TEST_ID); + expect(data.labels).toEqual([]); + expect(data.datasets).toEqual([]); + }); +}); diff --git a/src/components/BarChart/BarChart.tsx b/src/components/BarChart/BarChart.tsx new file mode 100644 index 0000000..03d7d47 --- /dev/null +++ b/src/components/BarChart/BarChart.tsx @@ -0,0 +1,57 @@ +import React, { useMemo } from 'react'; +import { + Chart as ChartJS, + CategoryScale, + LinearScale, + BarElement, + Tooltip, + Legend, +} from 'chart.js'; +import { Bar } from 'react-chartjs-2'; +import { buildChartConfig } from '../../utils/buildChartConfig'; +import type { BarChartProps } from './BarChart.types'; + +ChartJS.register(CategoryScale, LinearScale, BarElement, Tooltip, Legend); + +/** + * BarChart renders a bar chart using react-chartjs-2. + * Supports stacked and horizontal variants via typed props only. + * + * @example + * ```tsx + * + * ``` + */ +export const BarChart: React.FC = ({ + data, + theme, + height = 300, + stacked = false, + horizontal = false, +}) => { + const config = useMemo(() => { + const base = buildChartConfig(data, theme, 'bar'); + + if (stacked) { + base.options.scales.x.stacked = true; + base.options.scales.y.stacked = true; + } + + if (horizontal) { + base.options.indexAxis = 'y'; + } + + return base; + }, [data, theme, stacked, horizontal]); + + return ( +
+ ['options']} + /> +
+ ); +}; + +BarChart.displayName = 'BarChart'; diff --git a/src/components/BarChart/BarChart.types.ts b/src/components/BarChart/BarChart.types.ts new file mode 100644 index 0000000..0a6db58 --- /dev/null +++ b/src/components/BarChart/BarChart.types.ts @@ -0,0 +1,14 @@ +import type { ChartDataset, ChartTheme } from '../../types/chart.types'; + +export interface BarChartProps { + /** Array of datasets to render */ + data: ChartDataset[]; + /** Theme configuration for colors, fonts, grid, tooltip, and legend */ + theme: ChartTheme; + /** Chart height in pixels @default 300 */ + height?: number; + /** Stack bars on top of each other */ + stacked?: boolean; + /** Render horizontal bars (indexAxis 'y') */ + horizontal?: boolean; +} diff --git a/src/components/BarChart/index.ts b/src/components/BarChart/index.ts new file mode 100644 index 0000000..e9b7e02 --- /dev/null +++ b/src/components/BarChart/index.ts @@ -0,0 +1,2 @@ +export { BarChart } from './BarChart'; +export type { BarChartProps } from './BarChart.types'; diff --git a/src/components/LineChart/LineChart.test.tsx b/src/components/LineChart/LineChart.test.tsx new file mode 100644 index 0000000..1d6d890 --- /dev/null +++ b/src/components/LineChart/LineChart.test.tsx @@ -0,0 +1,59 @@ +import { describe, it, expect, vi } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import React from 'react'; +import { + singleDataset, + multiDatasets, + defaultTheme, + getCanvasData, + describeCommonChartBehavior, +} from '../../__tests__/chart-test-utils'; + +vi.mock('react-chartjs-2', () => ({ + Line: (props: { data: unknown; options: unknown }) => ( + + ), +})); + +vi.mock('chart.js', () => ({ + Chart: { register: vi.fn() }, + CategoryScale: 'CategoryScale', + LinearScale: 'LinearScale', + LineElement: 'LineElement', + PointElement: 'PointElement', + Filler: 'Filler', + Tooltip: 'Tooltip', + Legend: 'Legend', +})); + +import { LineChart } from './LineChart'; + +const CANVAS_TEST_ID = 'line-canvas'; + +describe('LineChart', () => { + describeCommonChartBehavior(LineChart, CANVAS_TEST_ID); + + it('should not set tension when smooth is false', () => { + render(); + const { data } = getCanvasData(CANVAS_TEST_ID); + expect(data.datasets[0].tension).toBeUndefined(); + }); + + it('should set tension 0.4 on all datasets when smooth is true', () => { + render(); + const { data } = getCanvasData(CANVAS_TEST_ID); + for (const ds of data.datasets) { + expect(ds.tension).toBe(0.4); + } + }); + + it('should not set fill for line variant', () => { + render(); + const { data } = getCanvasData(CANVAS_TEST_ID); + expect(data.datasets[0].fill).toBeUndefined(); + }); +}); diff --git a/src/components/LineChart/LineChart.tsx b/src/components/LineChart/LineChart.tsx new file mode 100644 index 0000000..b8e2f8d --- /dev/null +++ b/src/components/LineChart/LineChart.tsx @@ -0,0 +1,55 @@ +import React, { useMemo } from 'react'; +import { + Chart as ChartJS, + CategoryScale, + LinearScale, + LineElement, + PointElement, + Filler, + Tooltip, + Legend, +} from 'chart.js'; +import { Line } from 'react-chartjs-2'; +import { buildChartConfig } from '../../utils/buildChartConfig'; +import type { LineChartProps } from './LineChart.types'; + +ChartJS.register(CategoryScale, LinearScale, LineElement, PointElement, Filler, Tooltip, Legend); + +/** + * LineChart renders a line chart using react-chartjs-2. + * Supports smooth curves via the smooth prop. + * + * @example + * ```tsx + * + * ``` + */ +export const LineChart: React.FC = ({ + data, + theme, + height = 300, + smooth = false, +}) => { + const config = useMemo(() => { + const base = buildChartConfig(data, theme, 'line'); + + if (smooth) { + for (const ds of base.data.datasets) { + ds.tension = 0.4; + } + } + + return base; + }, [data, theme, smooth]); + + return ( +
+ ['options']} + /> +
+ ); +}; + +LineChart.displayName = 'LineChart'; diff --git a/src/components/LineChart/LineChart.types.ts b/src/components/LineChart/LineChart.types.ts new file mode 100644 index 0000000..6e62173 --- /dev/null +++ b/src/components/LineChart/LineChart.types.ts @@ -0,0 +1,12 @@ +import type { ChartDataset, ChartTheme } from '../../types/chart.types'; + +export interface LineChartProps { + /** Array of datasets to render */ + data: ChartDataset[]; + /** Theme configuration for colors, fonts, grid, tooltip, and legend */ + theme: ChartTheme; + /** Chart height in pixels @default 300 */ + height?: number; + /** Apply curved line interpolation (tension 0.4) @default false */ + smooth?: boolean; +} diff --git a/src/components/LineChart/index.ts b/src/components/LineChart/index.ts new file mode 100644 index 0000000..b22f5c7 --- /dev/null +++ b/src/components/LineChart/index.ts @@ -0,0 +1,2 @@ +export { LineChart } from './LineChart'; +export type { LineChartProps } from './LineChart.types'; diff --git a/src/components/index.ts b/src/components/index.ts index 52f8fa8..d5b415c 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -2,3 +2,6 @@ export const __components_placeholder = true; export * from './NoopButton'; +export * from './BarChart'; +export * from './LineChart'; +export * from './AreaChart'; diff --git a/src/index.ts b/src/index.ts index c55977d..14b0c36 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,4 @@ export * from './components'; export * from './hooks'; +export * from './types'; export * from './utils'; diff --git a/src/types/chart.types.ts b/src/types/chart.types.ts new file mode 100644 index 0000000..039a100 --- /dev/null +++ b/src/types/chart.types.ts @@ -0,0 +1,70 @@ +export interface ChartDataPoint { + label: string; + value: number; +} + +export interface ChartDataset { + id: string; + label: string; + data: ChartDataPoint[]; + color?: string; +} + +export interface ChartTheme { + colors: string[]; + fontFamily?: string; + fontSize?: number; + grid?: { + color?: string; + display?: boolean; + }; + tooltip?: { + enabled?: boolean; + backgroundColor?: string; + titleColor?: string; + bodyColor?: string; + }; + legend?: { + display?: boolean; + position?: 'top' | 'bottom' | 'left' | 'right'; + }; +} + +export type ChartVariant = 'bar' | 'line' | 'area'; + +export interface ChartConfig { + type: 'bar' | 'line'; + data: { + labels: string[]; + datasets: ChartConfigDataset[]; + }; + options: { + responsive: boolean; + indexAxis?: 'x' | 'y'; + plugins: { + tooltip: ChartTheme['tooltip'] & Record; + legend: ChartTheme['legend'] & Record; + }; + scales: { + x: { + grid: ChartTheme['grid'] & Record; + ticks: Record; + stacked?: boolean; + }; + y: { + grid: ChartTheme['grid'] & Record; + ticks: Record; + stacked?: boolean; + }; + }; + }; +} + +export interface ChartConfigDataset { + label: string; + data: number[]; + backgroundColor: string | string[]; + borderColor: string; + fill?: boolean; + tension?: number; +} diff --git a/src/types/index.ts b/src/types/index.ts new file mode 100644 index 0000000..9613f86 --- /dev/null +++ b/src/types/index.ts @@ -0,0 +1 @@ +export * from './chart.types'; diff --git a/src/utils/buildChartConfig.test.ts b/src/utils/buildChartConfig.test.ts new file mode 100644 index 0000000..e06023d --- /dev/null +++ b/src/utils/buildChartConfig.test.ts @@ -0,0 +1,168 @@ +import { describe, it, expect } from 'vitest'; +import { buildChartConfig } from './buildChartConfig'; +import type { ChartDataset, ChartTheme } from '../types/chart.types'; + +const sampleData = [ + { label: 'Jan', value: 10 }, + { label: 'Feb', value: 20 }, + { label: 'Mar', value: 30 }, +]; + +const baseTheme: ChartTheme = { + colors: ['#FF0000', '#00FF00', '#0000FF'], + fontFamily: 'Arial', + fontSize: 14, + grid: { color: '#E0E0E0', display: true }, + tooltip: { + enabled: true, + backgroundColor: '#333', + titleColor: '#FFF', + bodyColor: '#CCC', + }, + legend: { display: true, position: 'bottom' }, +}; + +const datasets: ChartDataset[] = [ + { id: 'ds1', label: 'Sales', data: sampleData }, + { id: 'ds2', label: 'Revenue', data: sampleData, color: '#ABCDEF' }, +]; + +describe('buildChartConfig', () => { + describe('bar variant', () => { + const config = buildChartConfig(datasets, baseTheme, 'bar'); + + it('should set type to bar', () => { + expect(config.type).toBe('bar'); + }); + + it('should extract labels from the first dataset', () => { + expect(config.data.labels).toEqual(['Jan', 'Feb', 'Mar']); + }); + + it('should map dataset values', () => { + expect(config.data.datasets[0].data).toEqual([10, 20, 30]); + }); + + it('should fall back to theme.colors when dataset.color is undefined', () => { + expect(config.data.datasets[0].backgroundColor).toBe('#FF0000'); + expect(config.data.datasets[0].borderColor).toBe('#FF0000'); + }); + + it('should use dataset.color when provided', () => { + expect(config.data.datasets[1].backgroundColor).toBe('#ABCDEF'); + expect(config.data.datasets[1].borderColor).toBe('#ABCDEF'); + }); + + it('should not set fill for bar variant', () => { + expect(config.data.datasets[0].fill).toBeUndefined(); + }); + }); + + describe('line variant', () => { + const config = buildChartConfig(datasets, baseTheme, 'line'); + + it('should set type to line', () => { + expect(config.type).toBe('line'); + }); + + it('should not set fill for line variant', () => { + expect(config.data.datasets[0].fill).toBeUndefined(); + }); + }); + + describe('area variant', () => { + const config = buildChartConfig(datasets, baseTheme, 'area'); + + it('should set type to line for area variant', () => { + expect(config.type).toBe('line'); + }); + + it('should set fill true for area datasets', () => { + expect(config.data.datasets[0].fill).toBe(true); + expect(config.data.datasets[1].fill).toBe(true); + }); + + it('should append 33 to hex color for 20% opacity background', () => { + expect(config.data.datasets[0].backgroundColor).toBe('#FF000033'); + }); + + it('should append 33 to explicit dataset color for area', () => { + expect(config.data.datasets[1].backgroundColor).toBe('#ABCDEF33'); + }); + + it('should keep borderColor without opacity suffix', () => { + expect(config.data.datasets[0].borderColor).toBe('#FF0000'); + }); + }); + + describe('theme fields reflected in output', () => { + const config = buildChartConfig(datasets, baseTheme, 'bar'); + + it('should reflect tooltip settings', () => { + expect(config.options.plugins.tooltip.enabled).toBe(true); + expect(config.options.plugins.tooltip.backgroundColor).toBe('#333'); + expect(config.options.plugins.tooltip.titleColor).toBe('#FFF'); + expect(config.options.plugins.tooltip.bodyColor).toBe('#CCC'); + }); + + it('should reflect legend settings', () => { + expect(config.options.plugins.legend.display).toBe(true); + expect(config.options.plugins.legend.position).toBe('bottom'); + }); + + it('should reflect grid settings on both axes', () => { + expect(config.options.scales.x.grid.display).toBe(true); + expect(config.options.scales.x.grid.color).toBe('#E0E0E0'); + expect(config.options.scales.y.grid.display).toBe(true); + expect(config.options.scales.y.grid.color).toBe('#E0E0E0'); + }); + + it('should reflect fontFamily and fontSize in tick fonts', () => { + expect(config.options.scales.x.ticks).toEqual({ font: { family: 'Arial', size: 14 } }); + expect(config.options.scales.y.ticks).toEqual({ font: { family: 'Arial', size: 14 } }); + }); + + it('should set responsive true', () => { + expect(config.options.responsive).toBe(true); + }); + }); + + describe('color cycling', () => { + it('should cycle through theme.colors when there are more datasets than colors', () => { + const threeDs: ChartDataset[] = [ + { id: '1', label: 'A', data: sampleData }, + { id: '2', label: 'B', data: sampleData }, + { id: '3', label: 'C', data: sampleData }, + { id: '4', label: 'D', data: sampleData }, + ]; + const config = buildChartConfig(threeDs, baseTheme, 'bar'); + expect(config.data.datasets[3].backgroundColor).toBe('#FF0000'); + }); + }); + + describe('empty datasets', () => { + it('should produce empty labels for empty datasets array', () => { + const config = buildChartConfig([], baseTheme, 'bar'); + expect(config.data.labels).toEqual([]); + expect(config.data.datasets).toEqual([]); + }); + }); + + describe('default theme values', () => { + const minTheme: ChartTheme = { colors: ['#111'] }; + const config = buildChartConfig(datasets, minTheme, 'line'); + + it('should default tooltip.enabled to true', () => { + expect(config.options.plugins.tooltip.enabled).toBe(true); + }); + + it('should default legend.display to true and position to top', () => { + expect(config.options.plugins.legend.display).toBe(true); + expect(config.options.plugins.legend.position).toBe('top'); + }); + + it('should default grid.display to true', () => { + expect(config.options.scales.x.grid.display).toBe(true); + }); + }); +}); diff --git a/src/utils/buildChartConfig.ts b/src/utils/buildChartConfig.ts new file mode 100644 index 0000000..0197b1c --- /dev/null +++ b/src/utils/buildChartConfig.ts @@ -0,0 +1,88 @@ +import type { + ChartConfig, + ChartConfigDataset, + ChartDataset, + ChartTheme, + ChartVariant, +} from '../types/chart.types'; + +function resolveColor(dataset: ChartDataset, index: number, theme: ChartTheme): string { + return dataset.color ?? theme.colors[index % theme.colors.length]; +} + +function applyAreaOpacity(hex: string): string { + return `${hex}33`; +} + +function buildDataset( + dataset: ChartDataset, + index: number, + theme: ChartTheme, + variant: ChartVariant, +): ChartConfigDataset { + const color = resolveColor(dataset, index, theme); + + const base: ChartConfigDataset = { + label: dataset.label, + data: dataset.data.map((point) => point.value), + backgroundColor: variant === 'area' ? applyAreaOpacity(color) : color, + borderColor: color, + }; + + if (variant === 'area') { + base.fill = true; + } + + return base; +} + +export function buildChartConfig( + datasets: ChartDataset[], + theme: ChartTheme, + variant: ChartVariant, +): ChartConfig { + const labels = datasets.length > 0 ? datasets[0].data.map((point) => point.label) : []; + + const tickFont: Record = {}; + if (theme.fontFamily) tickFont.family = theme.fontFamily; + if (theme.fontSize) tickFont.size = theme.fontSize; + + return { + type: variant === 'area' ? 'line' : variant, + data: { + labels, + datasets: datasets.map((ds, i) => buildDataset(ds, i, theme, variant)), + }, + options: { + responsive: true, + plugins: { + tooltip: { + enabled: theme.tooltip?.enabled ?? true, + backgroundColor: theme.tooltip?.backgroundColor, + titleColor: theme.tooltip?.titleColor, + bodyColor: theme.tooltip?.bodyColor, + }, + legend: { + display: theme.legend?.display ?? true, + position: theme.legend?.position ?? 'top', + }, + }, + scales: { + x: { + grid: { + display: theme.grid?.display ?? true, + color: theme.grid?.color, + }, + ticks: { font: tickFont }, + }, + y: { + grid: { + display: theme.grid?.display ?? true, + color: theme.grid?.color, + }, + ticks: { font: tickFont }, + }, + }, + }, + }; +} diff --git a/src/utils/index.ts b/src/utils/index.ts index 6211c64..5cdf899 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -2,3 +2,4 @@ export const __utils_placeholder = true; export * from './noop'; +export * from './buildChartConfig'; From f8fa8b92601bff000db01634006c04c925f0b65f Mon Sep 17 00:00:00 2001 From: Zaiidmo Date: Tue, 7 Apr 2026 09:43:23 +0100 Subject: [PATCH 09/14] ops: updated release check jobs --- .github/workflows/release-check.yml | 157 ++++++++++++++++++++++++---- 1 file changed, 134 insertions(+), 23 deletions(-) diff --git a/.github/workflows/release-check.yml b/.github/workflows/release-check.yml index 53792e7..2644af5 100644 --- a/.github/workflows/release-check.yml +++ b/.github/workflows/release-check.yml @@ -8,35 +8,39 @@ concurrency: group: ci-release-${{ github.ref }} cancel-in-progress: true +env: + SONAR_HOST_URL: 'https://sonarcloud.io' + SONAR_ORGANIZATION: 'ciscode' + SONAR_PROJECT_KEY: 'CISCODE-MA_ChartKit-UI' + NODE_VERSION: '22' + +# ─── Job 1: Static checks (fast feedback, runs in parallel with test) ────────── jobs: - ci: + quality: + name: Quality Checks runs-on: ubuntu-latest - timeout-minutes: 25 + timeout-minutes: 10 permissions: contents: read - statuses: write - - env: - SONAR_HOST_URL: 'https://sonarcloud.io' - SONAR_ORGANIZATION: 'ciscode' - SONAR_PROJECT_KEY: 'CISCODE-MA_ChartKit-UI' steps: - name: Checkout uses: actions/checkout@v4 - with: - fetch-depth: 0 - name: Setup Node uses: actions/setup-node@v4 with: - node-version: '22' + node-version: ${{ env.NODE_VERSION }} cache: 'npm' - name: Install run: npm ci + - name: Security Audit + # Only fail on high/critical — moderate noise in dev deps is expected + run: npm audit --production --audit-level=high + - name: Format run: npm run format @@ -46,12 +50,94 @@ jobs: - name: Lint run: npm run lint + # ─── Job 2: Tests + Coverage (artifact passed to Sonar) ──────────────────────── + test: + name: Test & Coverage + runs-on: ubuntu-latest + timeout-minutes: 15 + + permissions: + contents: read + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + + - name: Install + run: npm ci + - name: Test (with coverage) run: npm run test:cov + - name: Upload coverage report + uses: actions/upload-artifact@v4 + with: + name: coverage-report + path: coverage/ + retention-days: 1 + + # ─── Job 3: Build ────────────────────────────────────────────────────────────── + build: + name: Build + runs-on: ubuntu-latest + needs: [quality, test] + timeout-minutes: 10 + + permissions: + contents: read + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + + - name: Install + run: npm ci + - name: Build run: npm run build + # ─── Job 4: SonarCloud (depends on test for coverage data) ───────────────────── + sonar: + name: SonarCloud Analysis + runs-on: ubuntu-latest + needs: [test] + timeout-minutes: 15 + + permissions: + contents: read + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + # Full history required for accurate blame & new code detection + fetch-depth: 0 + + - name: Download coverage report + uses: actions/download-artifact@v4 + with: + name: coverage-report + path: coverage/ + + - name: Cache SonarCloud packages + uses: actions/cache@v4 + with: + path: ~/.sonar/cache + key: sonar-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }} + restore-keys: sonar-${{ runner.os }}- + - name: SonarCloud Scan uses: SonarSource/sonarqube-scan-action@v6 env: @@ -62,19 +148,42 @@ jobs: -Dsonar.organization=${{ env.SONAR_ORGANIZATION }} -Dsonar.projectKey=${{ env.SONAR_PROJECT_KEY }} -Dsonar.sources=src - -Dsonar.tests=src/__tests__ - -Dsonar.exclusions=src/__tests__/** + -Dsonar.tests=test + -Dsonar.test.inclusions=**/*.spec.ts,**/*.test.ts + -Dsonar.exclusions=**/node_modules/**,**/dist/**,**/coverage/**,**/*.d.ts + -Dsonar.coverage.exclusions=**/*.spec.ts,**/*.test.ts,**/index.ts -Dsonar.javascript.lcov.reportPaths=coverage/lcov.info + -Dsonar.typescript.tsconfigPath=tsconfig.json + -Dsonar.qualitygate.wait=true + -Dsonar.qualitygate.timeout=300 - - name: SonarCloud Quality Gate - uses: SonarSource/sonarqube-quality-gate-action@v1 - timeout-minutes: 10 - env: - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - SONAR_HOST_URL: ${{ env.SONAR_HOST_URL }} + # ─── Job 5: Final status report (always runs) ────────────────────────────────── + report: + name: Report CI Status + runs-on: ubuntu-latest + needs: [quality, test, build, sonar] + # Run even if upstream jobs failed + if: always() + timeout-minutes: 5 + + permissions: + contents: read + statuses: write - - name: Report CI status - if: always() + steps: + - name: Resolve overall result + id: result + run: | + results="${{ needs.quality.result }} ${{ needs.test.result }} ${{ needs.build.result }} ${{ needs.sonar.result }}" + if echo "$results" | grep -qE "failure|cancelled"; then + echo "state=failure" >> $GITHUB_OUTPUT + echo "desc=One or more CI checks failed" >> $GITHUB_OUTPUT + else + echo "state=success" >> $GITHUB_OUTPUT + echo "desc=All CI checks passed" >> $GITHUB_OUTPUT + fi + + - name: Post commit status uses: actions/github-script@v7 with: script: | @@ -82,6 +191,8 @@ jobs: owner: context.repo.owner, repo: context.repo.repo, sha: context.sha, - state: '${{ job.status }}' === 'success' ? 'success' : 'failure', - description: 'CI checks completed' + state: '${{ steps.result.outputs.state }}', + context: 'CI / Release Check', + description: '${{ steps.result.outputs.desc }}', + target_url: `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}` }) From 32e7e5b0a1c73b54221ba847732a58a271afbf79 Mon Sep 17 00:00:00 2001 From: Zaiidmo Date: Tue, 7 Apr 2026 09:43:50 +0100 Subject: [PATCH 10/14] install dep --- package-lock.json | 38 +++++++++++++------------------------- 1 file changed, 13 insertions(+), 25 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1599bb6..c516044 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@ciscode/ui-chart-kit", - "version": "0.0.0", + "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@ciscode/ui-chart-kit", - "version": "0.0.0", + "version": "0.1.0", "license": "MIT", "dependencies": { "chart.js": "^4.5.1", @@ -162,7 +162,6 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -784,7 +783,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=20.19.0" }, @@ -825,7 +823,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=20.19.0" } @@ -2322,7 +2319,8 @@ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@types/estree": { "version": "1.0.8", @@ -2351,7 +2349,6 @@ "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" @@ -2363,7 +2360,6 @@ "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", "dev": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^18.0.0" } @@ -2403,7 +2399,6 @@ "integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/types": "8.56.1", @@ -2739,7 +2734,6 @@ "integrity": "sha512-izzd2zmnk8Nl5ECYkW27328RbQ1nKvkm6Bb5DAaz1Gk59EbLkiCMa6OLT0NoaAYTjOFS6N+SMYW1nh4/9ljPiw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@vitest/utils": "2.1.9", "fflate": "^0.8.2", @@ -2777,7 +2771,6 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3174,7 +3167,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -3342,7 +3334,6 @@ "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz", "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==", "license": "MIT", - "peer": true, "dependencies": { "@kurkle/color": "^0.3.0" }, @@ -3743,7 +3734,8 @@ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/dunder-proto": { "version": "1.0.1", @@ -4012,7 +4004,6 @@ "dev": true, "hasInstallScript": true, "license": "MIT", - "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -4077,7 +4068,6 @@ "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -5870,7 +5860,6 @@ "integrity": "sha512-0+MoQNYyr2rBHqO1xilltfDjV9G7ymYGlAUazgcDLQaUf8JDHbuGwsxN6U9qWaElZ4w1B2r7yEGIL3GdeW3Rug==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@acemir/cssom": "^0.9.31", "@asamuzakjp/dom-selector": "^6.8.1", @@ -6250,6 +6239,7 @@ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -7007,7 +6997,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -7092,6 +7081,7 @@ "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -7107,6 +7097,7 @@ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=10" }, @@ -7219,7 +7210,8 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/read-yaml-file": { "version": "1.1.0", @@ -7596,7 +7588,8 @@ "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/semver": { "version": "7.7.4", @@ -8415,7 +8408,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -8702,7 +8694,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -8828,7 +8819,6 @@ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", @@ -9342,7 +9332,6 @@ "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@vitest/expect": "2.1.9", "@vitest/mocker": "2.1.9", @@ -9753,7 +9742,6 @@ "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "dev": true, "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } From 8769de6e19834cb97adf732803cb6eb82212b0a3 Mon Sep 17 00:00:00 2001 From: Zaiidmo Date: Tue, 7 Apr 2026 10:51:08 +0100 Subject: [PATCH 11/14] fix(ci): correct sonar test paths and normalize repository url --- .github/workflows/release-check.yml | 6 +++--- package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release-check.yml b/.github/workflows/release-check.yml index 2644af5..9b17249 100644 --- a/.github/workflows/release-check.yml +++ b/.github/workflows/release-check.yml @@ -148,10 +148,10 @@ jobs: -Dsonar.organization=${{ env.SONAR_ORGANIZATION }} -Dsonar.projectKey=${{ env.SONAR_PROJECT_KEY }} -Dsonar.sources=src - -Dsonar.tests=test - -Dsonar.test.inclusions=**/*.spec.ts,**/*.test.ts + -Dsonar.tests=src + -Dsonar.test.inclusions=**/*.spec.ts,**/*.spec.tsx,**/*.test.ts,**/*.test.tsx -Dsonar.exclusions=**/node_modules/**,**/dist/**,**/coverage/**,**/*.d.ts - -Dsonar.coverage.exclusions=**/*.spec.ts,**/*.test.ts,**/index.ts + -Dsonar.coverage.exclusions=**/*.spec.ts,**/*.spec.tsx,**/*.test.ts,**/*.test.tsx,**/index.ts -Dsonar.javascript.lcov.reportPaths=coverage/lcov.info -Dsonar.typescript.tsconfigPath=tsconfig.json -Dsonar.qualitygate.wait=true diff --git a/package.json b/package.json index 7ef117a..6ba0d39 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "type": "module", "repository": { "type": "git", - "url": "git+https://github.com/CISCODE-MA/ChartKit-UI.git" + "url": "https://github.com/CISCODE-MA/ChartKit-UI" }, "sideEffects": false, "files": [ From bb6e64599deda457bbc8f556a1eb97b542c8b6d0 Mon Sep 17 00:00:00 2001 From: Zaiidmo Date: Tue, 7 Apr 2026 10:51:10 +0100 Subject: [PATCH 12/14] test(lint): remove unused imports in chart component tests --- src/components/AreaChart/AreaChart.test.tsx | 2 +- src/components/BarChart/BarChart.test.tsx | 3 +-- src/components/LineChart/LineChart.test.tsx | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/components/AreaChart/AreaChart.test.tsx b/src/components/AreaChart/AreaChart.test.tsx index 75274ca..b23a4b0 100644 --- a/src/components/AreaChart/AreaChart.test.tsx +++ b/src/components/AreaChart/AreaChart.test.tsx @@ -1,5 +1,5 @@ import { describe, it, expect, vi } from 'vitest'; -import { render, screen } from '@testing-library/react'; +import { render } from '@testing-library/react'; import React from 'react'; import { singleDataset, diff --git a/src/components/BarChart/BarChart.test.tsx b/src/components/BarChart/BarChart.test.tsx index da80a97..7891754 100644 --- a/src/components/BarChart/BarChart.test.tsx +++ b/src/components/BarChart/BarChart.test.tsx @@ -1,9 +1,8 @@ import { describe, it, expect, vi } from 'vitest'; -import { render, screen } from '@testing-library/react'; +import { render } from '@testing-library/react'; import React from 'react'; import { singleDataset, - multiDatasets, defaultTheme, getCanvasData, describeCommonChartBehavior, diff --git a/src/components/LineChart/LineChart.test.tsx b/src/components/LineChart/LineChart.test.tsx index 1d6d890..e034c77 100644 --- a/src/components/LineChart/LineChart.test.tsx +++ b/src/components/LineChart/LineChart.test.tsx @@ -1,5 +1,5 @@ import { describe, it, expect, vi } from 'vitest'; -import { render, screen } from '@testing-library/react'; +import { render } from '@testing-library/react'; import React from 'react'; import { singleDataset, From 4e12852e450f01cbcb6d78f052386d7db473b397 Mon Sep 17 00:00:00 2001 From: Zaiidmo Date: Tue, 7 Apr 2026 10:54:44 +0100 Subject: [PATCH 13/14] chore: updated versions .. preparing release --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6ba0d39..5332af8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@ciscode/ui-chart-kit", - "version": "0.1.0", + "version": "0.0.0", "description": "Typed React chart components (Bar, Line, Area) built on Chart.js — no raw Chart.js config required.", "license": "MIT", "private": false, From 0a7afd8a540280488e1397be28d14fd2167c0d41 Mon Sep 17 00:00:00 2001 From: Zaiidmo Date: Tue, 7 Apr 2026 10:54:49 +0100 Subject: [PATCH 14/14] 0.0.1 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index c516044..f8e20c1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@ciscode/ui-chart-kit", - "version": "0.1.0", + "version": "0.0.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@ciscode/ui-chart-kit", - "version": "0.1.0", + "version": "0.0.1", "license": "MIT", "dependencies": { "chart.js": "^4.5.1", diff --git a/package.json b/package.json index 5332af8..3bfc1fc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@ciscode/ui-chart-kit", - "version": "0.0.0", + "version": "0.0.1", "description": "Typed React chart components (Bar, Line, Area) built on Chart.js — no raw Chart.js config required.", "license": "MIT", "private": false,