Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
23 changes: 23 additions & 0 deletions .changeset/COMPT-31-dom-event-hooks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
"@ciscode/hooks-kit": minor
---

feat(COMPT-31): add DOM & event hooks — useMediaQuery, useWindowSize, useClickOutside, useIntersectionObserver

Second batch of production-ready hooks for HooksKit (epic COMPT-2).

**New hooks:**

- `useMediaQuery(query)` — tracks `matchMedia`, updates on change via `useSyncExternalStore`, SSR-safe (server snapshot returns `false`)
- `useWindowSize()` — returns `{ width, height }`, debounced 100ms on resize, SSR-safe (returns `{ 0, 0 }`)
- `useClickOutside(ref, handler)` — fires on `mousedown` or `touchstart` outside ref element, handler updated via ref pattern to avoid stale closures
- `useIntersectionObserver(ref, options?)` — returns latest `IntersectionObserverEntry | null`, disconnects observer on unmount

**Implementation details:**

- All listeners registered in `useEffect` and removed in cleanup return
- All SSR-safe: `typeof window === 'undefined'` guards in every hook
- `useMediaQuery` uses `useSyncExternalStore` (React 18) — no `setState` in effects
- Zero runtime dependencies
- `tsc --noEmit` passes, ESLint passes (0 warnings), 26/26 tests pass, coverage ≥ 95%
- All four hooks exported from `src/index.ts`
20 changes: 20 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -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
74 changes: 39 additions & 35 deletions .github/instructions/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -1,58 +1,62 @@
# Copilot Instructions - React Component Library
# Copilot Instructions - HooksKit

> **Purpose**: Development guidelines for React component libraries - reusable, well-structured components for modern apps.
> **Purpose**: Development guidelines for HooksKit — production-ready React hooks with zero runtime deps.

---

## 🎯 Module Overview

**Package**: `@ciscode/ui-components` (example)
**Type**: React Component Library
**Package**: `@ciscode/hooks-kit`
**Epic**: COMPT-2 — HooksKit
**Type**: React Hooks Library
**Framework**: React 18+, TypeScript 5+
**Build**: Vite/tsup
**Build**: tsup
**Testing**: Vitest + React Testing Library
**Distribution**: NPM package
**Purpose**: Reusable, production-ready React components for building modern UIs
**Purpose**: 12 production-ready React hooks. Zero runtime deps. SSR-safe.

### Typical Module Responsibilities:
### Hook Groups:

- Atomic UI components (Button, Input, Card, etc.)
- Composite components (Form, Modal, Navigation, etc.)
- Hooks for common patterns
- Type definitions and props interfaces
- Accessibility compliance (WCAG 2.1 AA)
- Theming and customization
- Comprehensive documentation
- **State & Storage** (COMPT-30 ✅) — `useDebounce`, `useLocalStorage`, `useSessionStorage`
- **DOM & Events** (COMPT-31 ✅) — `useMediaQuery`, `useWindowSize`, `useClickOutside`, `useIntersectionObserver`
- **Async & Lifecycle** — _(upcoming)_

### Module Responsibilities:

- Generic, fully-typed hooks with inference at call site
- SSR-safe (`typeof window === 'undefined'` guards in every hook)
- Zero runtime dependencies
- All listeners registered in `useEffect` and cleaned up on unmount
- WCAG-accessible patterns where applicable
- Hooks ≥ 90% coverage

---

## 🏗️ Module Structure

```
src/
├── components/ # React components
│ ├── Button/
│ │ ├── Button.tsx # Component
│ │ ├── Button.test.tsx # Tests
│ │ ├── Button.types.ts # Props types
│ │ └── index.ts # Exports
│ ├── Input/
│ ├── Modal/
│ └── Form/
├── hooks/ # Custom hooks
│ ├── useModal.ts
│ ├── useForm.ts
│ └── useModal.test.ts
├── context/ # Context providers
│ ├── ThemeContext.tsx
│ └── FormContext.tsx
├── types/ # TypeScript types
│ └── common.types.ts
├── utils/ # Utilities
│ └── classNameUtils.ts
└── index.ts # Public API
├── components/ # Minimal supporting components
│ ├── NoopButton.tsx
│ └── index.ts
├── hooks/ # All public hooks
│ ├── useDebounce.ts # COMPT-30 ✅
│ ├── useLocalStorage.ts # COMPT-30 ✅
│ ├── useSessionStorage.ts # COMPT-30 ✅
│ ├── storage.ts # Internal SSR-safe storage helper
│ ├── useMediaQuery.ts # COMPT-31 ✅
│ ├── useWindowSize.ts # COMPT-31 ✅
│ ├── useClickOutside.ts # COMPT-31 ✅
│ ├── useIntersectionObserver.ts # COMPT-31 ✅
│ └── index.ts # Hook barrel
├── utils/ # Framework-agnostic utils
│ ├── noop.ts
│ └── index.ts
└── index.ts # Public API (only entry point)
```

> ⚠️ Only export from `src/index.ts`. Deep imports are forbidden.

---

## 📝 Naming Conventions
Expand Down
41 changes: 41 additions & 0 deletions .github/workflows/pr-validation.yml
Original file line number Diff line number Diff line change
@@ -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: 22
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
46 changes: 34 additions & 12 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 <feat/your-feature> --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"
Comment on lines +55 to +57
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

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

The publish gate only checks that tag v${package.json version} exists somewhere in the repo (git rev-parse), not that it points to the commit being published. This can allow publishing from an untagged commit or reusing an old tag when package.json wasn’t bumped, leading to failed or incorrect publishes. Validate that TAG is an exact match on HEAD (e.g., git describe --exact-match --tags HEAD equals TAG) before proceeding.

Suggested change
echo "✅ package.json version: $PKG_VERSION"
echo "✅ Tag $TAG exists in repo"
# Ensure that the tag for this version is exactly on the commit being published (HEAD)
HEAD_TAG=$(git describe --exact-match --tags HEAD 2>/dev/null || true)
if [[ -z "$HEAD_TAG" ]]; then
echo "❌ ERROR: HEAD is not tagged."
echo "Expected HEAD to be tagged with $TAG for publishing."
exit 1
fi
if [[ "$HEAD_TAG" != "$TAG" ]]; then
HEAD_COMMIT=$(git rev-parse HEAD)
TAG_COMMIT=$(git rev-parse "$TAG")
echo "❌ ERROR: Tag mismatch for publish."
echo " package.json version: $PKG_VERSION"
echo " Expected tag on HEAD : $TAG"
echo " Actual tag on HEAD : $HEAD_TAG"
echo " HEAD commit : $HEAD_COMMIT"
echo " $TAG commit : $TAG_COMMIT"
echo ""
echo "Ensure you are publishing from the commit tagged with $TAG."
exit 1
fi
echo "✅ package.json version: $PKG_VERSION"
echo "✅ Tag $TAG exists in repo"
echo "✅ Tag $TAG matches HEAD"

Copilot uses AI. Check for mistakes.
echo "TAG_VERSION=$TAG" >> $GITHUB_ENV

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
registry-url: 'https://registry.npmjs.org'
cache: npm
node-version: "22"
registry-url: "https://registry.npmjs.org"
cache: "npm"

- name: Install dependencies
run: npm install
run: npm ci

- name: Build
run: npm run build --if-present
Expand All @@ -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 }}
38 changes: 11 additions & 27 deletions .github/workflows/release-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand All @@ -27,30 +17,25 @@ 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'
SONAR_PROJECT_KEY: 'CISCODE-MA_PACKAGE_NAME_TEMPLATE'
SONAR_HOST_URL: "https://sonarcloud.io"
SONAR_ORGANIZATION: "ciscode"
SONAR_PROJECT_KEY: "CISCODE-MA_HooksKit"

steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

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

This workflow uses actions/checkout@v6 while the rest of the repo uses @v4. Align on a single version and verify the chosen major version is valid/supported to avoid CI breakage due to an invalid action ref.

Suggested change
uses: actions/checkout@v6
uses: actions/checkout@v4

Copilot uses AI. Check for mistakes.
with:
fetch-depth: 0

- 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
Expand All @@ -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:
Comment on lines 55 to 60
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

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

SonarCloud steps run unconditionally on pull_request. For PRs from forks, secrets.SONAR_TOKEN is not available, so the workflow will fail. Consider gating these steps (e.g., if: github.event.pull_request.head.repo.fork == false) or using a safe alternative like pull_request_target with appropriate hardening.

Copilot uses AI. Check for mistakes.
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
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

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

-Dsonar.tests=test points to a directory that doesn’t exist in this repo (tests live under src/). This will cause Sonar to miss tests / miscompute coverage. Update sonar.tests (and optionally sonar.test.inclusions) to match the actual test locations.

Suggested change
-Dsonar.tests=test
-Dsonar.tests=src
-Dsonar.test.inclusions=src/**/*.test.*,src/**/*.spec.*

Copilot uses AI. Check for mistakes.
-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:
Expand Down
10 changes: 7 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@ciscode/reactts-developerkit",
"version": "1.0.0",
"description": "React TypeScript hybrid library template (components + hooks + utils).",
"name": "@ciscode/hooks-kit",
"version": "0.0.0",
"description": "12 production-ready React hooks. Zero runtime deps. SSR-safe. Groups: state and storage / DOM and events / async and lifecycle.",
"license": "MIT",
"private": false,
"type": "module",
Expand All @@ -16,6 +16,10 @@
"require": "./dist/index.cjs"
}
},
"repository": {
"type": "git",
"url": "git+https://github.com/CISCODE-MA/HooksKit.git"
},
"main": "./dist/index.cjs",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
Expand Down
4 changes: 4 additions & 0 deletions src/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
// Example placeholder export — replace with real hooks later.
export const __hooks_placeholder = true;

Comment on lines 1 to 3
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

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

The __hooks_placeholder export is still present and will be part of the published public API because src/index.ts re-exports ./hooks. Now that real hooks are exported here, the placeholder should be removed to avoid leaking a non-production symbol to consumers.

Suggested change
// Example placeholder export — replace with real hooks later.
export const __hooks_placeholder = true;

Copilot uses AI. Check for mistakes.
export * from './useClickOutside';
export * from './useIntersectionObserver';
export * from './useMediaQuery';
export * from './useWindowSize';
export * from './useNoop';
Loading
Loading