Skip to content

Develop#12

Merged
Zaiidmo merged 18 commits intomasterfrom
develop
Apr 7, 2026
Merged

Develop#12
Zaiidmo merged 18 commits intomasterfrom
develop

Conversation

@a-elkhiraooui-ciscode
Copy link
Copy Markdown
Contributor

Summary

  • What does this PR change?

Why

  • Why is this change needed?

Checklist

  • Added/updated tests (if behavior changed)
  • npm run lint passes
  • npm run typecheck passes
  • npm test passes
  • npm run build passes
  • Added a changeset (npx changeset) if this affects consumers

Notes

  • Anything reviewers should pay attention to?

Zaiidmo and others added 16 commits March 29, 2026 12:50
…ckages

- 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
- Run prettier --write across repository
- No logic/behavior changes
- Unblocks CI PR Validation format gate for feature PRs
- verify passes: lint + typecheck + tests
* feat(COMPT-30): add useDebounce, useLocalStorage, useSessionStorage hooks

- useDebounce<T>(value, delay): returns debounced value, resets timer on value/delay change
- useLocalStorage<T>(key, initial): syncs with localStorage, SSR-safe, JSON serialization
- useSessionStorage<T>(key, initial): same pattern for sessionStorage
- Shared storage.ts helper with readStorageValue/writeStorageValue (SSR guard + parse fallback)
- All three exported from src/hooks/index.ts -> src/index.ts
- Full test coverage: timer reset, JSON sync, parse error fallback, SSR guard
- tsc --noEmit passes, lint passes (0 warnings), 13/13 tests pass

* chore(COMPT-30): add changeset, update copilot-instructions, fix husky pre-commit

- .changeset/COMPT-30-state-storage-hooks.md: minor bump summary for 0.0.1 release
- .github/instructions/copilot-instructions.md: updated to HooksKit package identity,
  real src structure with COMPT-30 hooks marked, COMPT-XX branch naming convention
- .husky/pre-commit: removed deprecated husky v9 shebang lines (breaks in v10)
* feat(COMPT-31): add useMediaQuery, useWindowSize, useClickOutside, useIntersectionObserver

- useMediaQuery(query): tracks matchMedia via useSyncExternalStore, SSR-safe (server snapshot false)
- useWindowSize(): returns {width, height}, debounced 100ms on resize, SSR-safe ({0,0})
- useClickOutside(ref, handler): fires on mousedown/touchstart outside ref; handler via ref pattern
- useIntersectionObserver(ref, options?): IntersectionObserverEntry|null, disconnects on unmount
- All listeners registered in useEffect with cleanup return
- All SSR-safe: typeof window === undefined guards
- Zero runtime dependencies
- tsc --noEmit passes, lint passes (0 warnings), 26/26 tests pass, coverage >= 95%
- All four exported from src/hooks/index.ts -> src/index.ts
- Changeset added, copilot-instructions.md updated for epic COMPT-2

* test(COMPT-31): reduce duplicated test blocks for Sonar quality gate

- remove accidental duplicated useMediaQuery suite block
- extract shared viewport setup in useWindowSize tests
- extract shared mount helper in useClickOutside tests
- keep behavior coverage unchanged
* feat(COMPT-32): add usePrevious, useToggle, useInterval, useTimeout, useIsFirstRender

- usePrevious<T>(value): previous render value via state-derivation; undefined on first render
- useToggle(initial?): boolean toggle with stable useCallback reference
- useInterval(callback, delay|null): fires at cadence, stops on null, latest callback via ref
- useTimeout(callback, delay|null): fires once, cancels on null or unmount, latest callback via ref
- useIsFirstRender(): true only on first render (scoped eslint-disable for intentional ref access)
- All timer cleanup in useEffect return — StrictMode safe
- Zero runtime deps; tsc --noEmit passes, lint passes, 25/25 tests, hooks coverage >= 98%
- All five exported from src/hooks/index.ts -> src/index.ts
- Changeset added, copilot-instructions.md updated with all three COMPT groups complete

* refactor(COMPT-32): move hook tests to src/hooks/__tests__/

- Moved all 5 hook test files from src/hooks/ to src/hooks/__tests__/
- Updated relative imports from ./hook to ../hook
- No logic changes; all 25 tests still pass
…10)

- Move useDebounce, useLocalStorage, useSessionStorage, useMediaQuery,
  useWindowSize, useClickOutside, useIntersectionObserver tests to __tests__/
- Update relative imports from ./ to ../
- All 12 hooks covered: fake timers for debounce/interval/timeout/windowSize,
  mock matchMedia, mock IntersectionObserver, storage parse-error guards
- 55 tests passing, 97.44% line coverage (>= 85% AC)
- Full README with installation, SSR note, 12 hook examples with types
- Remove __hooks_placeholder from hooks barrel
- Fix COMPT-30 changeset package name (reactts-developerkit -> hooks-kit)
- Apply changeset version bump: 0.0.0 -> 0.1.0
- Remove duplicate root-level test files (already in __tests__/)
- Update copilot-instructions.md with COMPT-34 status

Co-authored-by: a-elkhiraooui-ciscode <a.elkhiraoui@ciscod.com>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR converts the repository from a template placeholder into the @ciscode/hooks-kit package by adding a hook suite (with tests), updating documentation/metadata, and tightening CI/release automation.

Changes:

  • Added a set of reusable React hooks under src/hooks/ plus a shared storage helper.
  • Added Vitest + React Testing Library coverage for the new hooks under src/hooks/__tests__/.
  • Updated package metadata/docs and expanded CI workflows (quality/test/build/Sonar) + publishing validation.

Reviewed changes

Copilot reviewed 36 out of 37 changed files in this pull request and generated 13 comments.

Show a summary per file
File Description
src/hooks/useWindowSize.ts Adds debounced resize-based window size hook with SSR guard.
src/hooks/useToggle.ts Adds boolean toggle hook with stable callback.
src/hooks/useTimeout.ts Adds timeout hook that keeps the latest callback via ref.
src/hooks/useSessionStorage.ts Adds sessionStorage-backed state hook using shared helper.
src/hooks/usePrevious.ts Adds previous-value hook implementation.
src/hooks/useMediaQuery.ts Adds media-query hook using useSyncExternalStore.
src/hooks/useLocalStorage.ts Adds localStorage-backed state hook using shared helper.
src/hooks/useIsFirstRender.ts Adds first-render detection hook.
src/hooks/useInterval.ts Adds interval hook that keeps the latest callback via ref.
src/hooks/useIntersectionObserver.ts Adds intersection observer hook.
src/hooks/useDebounce.ts Adds debounced value hook.
src/hooks/useClickOutside.ts Adds outside-click/touch hook with handler ref.
src/hooks/storage.ts Adds shared JSON + SSR-safe storage read/write helpers.
src/hooks/index.ts Replaces placeholder export with hook barrel exports.
src/hooks/tests/useWindowSize.test.ts Adds tests for window size behavior + debounce + SSR guard.
src/hooks/tests/useToggle.test.ts Adds tests for toggle behavior + stable callback.
src/hooks/tests/useTimeout.test.ts Adds tests for timeout behavior, cancellation, cleanup, latest callback.
src/hooks/tests/useSessionStorage.test.ts Adds tests for sessionStorage integration + parse fallback.
src/hooks/tests/usePrevious.test.ts Adds tests for previous-value behavior.
src/hooks/tests/useMediaQuery.test.ts Adds tests for matchMedia integration and updates.
src/hooks/tests/useLocalStorage.test.ts Adds tests for localStorage integration + parse fallback.
src/hooks/tests/useIsFirstRender.test.ts Adds tests for first-render detection behavior.
src/hooks/tests/useInterval.test.ts Adds tests for interval cadence, cancellation, cleanup, latest callback.
src/hooks/tests/useIntersectionObserver.test.ts Adds tests using a mocked IntersectionObserver.
src/hooks/tests/useDebounce.test.ts Adds tests for debounced update timing.
src/hooks/tests/useClickOutside.test.ts Adds tests for outside click/touch behavior + cleanup.
README.md Replaces template README with HooksKit documentation and examples.
package.json Renames package, updates description/version, adds repository metadata.
CHANGELOG.md Adds v0.1.0 changelog entries describing hook batches and docs/tests.
.husky/pre-commit Changes pre-commit hook to run lint-staged (but currently missing shebang/header).
.github/workflows/release-check.yml Splits CI into quality/test/build/sonar jobs and adds reporting.
.github/workflows/publish.yml Tightens publish validation + uses npm provenance.
.github/workflows/pr-validation.yml Adds PR validation workflow for develop.
.github/instructions/copilot-instructions.md Updates repo guidelines from component template to HooksKit.
.github/dependabot.yml Adds monthly Dependabot updates for npm dependencies.
.github/CODEOWNERS Adds default code owners.
Comments suppressed due to low confidence (1)

README.md:506

  • The README’s “Scripts”/“License” area has leftover bullets (lint/format/changeset) after the License section, and the “Release flow” section still refers to this repo as a template. Please move the bullets into “Scripts” (or remove them) and update/remove the template wording so the docs match HooksKit’s purpose.
## Scripts

```bash
npm run build       # Build to dist/ (ESM + CJS + types)
npm test            # Run tests (vitest)
npm run typecheck   # TypeScript typecheck
npm run verify      # Lint + typecheck + tests + coverage

License

MIT — see LICENSE.

  • npm run lint – ESLint
  • npm run format / npm run format:write – Prettier
  • npx changeset – create a changeset

Release flow (summary)

  • Work on a feature branch from develop
  • Merge to develop
  • Add a changeset for user-facing changes: npx changeset
  • Promote developmaster
  • Tag vX.Y.Z to publish (npm OIDC)

This repository is a template. Teams should clone it and focus only on
library logic, not tooling or release mechanics.

</details>

Comment on lines +3 to +8
export function usePrevious<T>(value: T): T | undefined {
const [[prev, curr], setState] = useState<[T | undefined, T]>([undefined, value]);

if (curr !== value) {
setState([curr, value]);
}
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

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

usePrevious is calling setState during render when value changes. This pattern can trigger React warnings/errors (and can be problematic in concurrent rendering) and always forces an extra render per change. Prefer tracking the previous value via a useRef updated in an effect (or layout effect) so no state updates occur during render.

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +21
import { type RefObject, useEffect, useRef, useState } from 'react';

export function useIntersectionObserver(
ref: RefObject<Element | null>,
options?: IntersectionObserverInit,
): IntersectionObserverEntry | null {
const [entry, setEntry] = useState<IntersectionObserverEntry | null>(null);
const optionsRef = useRef(options);

useEffect(() => {
if (typeof window === 'undefined' || !ref.current) return;

const observer = new IntersectionObserver(([newEntry]) => {
if (newEntry) setEntry(newEntry);
}, optionsRef.current);

observer.observe(ref.current);
return () => {
observer.disconnect();
};
}, [ref]);
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

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

useIntersectionObserver stores options in a ref only on initial render (useRef(options)), so later options changes are ignored. This can lead to observers using stale thresholds/root/rootMargin; consider including options in the effect dependencies (or updating optionsRef.current in an effect) and recreating the observer when options change.

Suggested change
import { type RefObject, useEffect, useRef, useState } from 'react';
export function useIntersectionObserver(
ref: RefObject<Element | null>,
options?: IntersectionObserverInit,
): IntersectionObserverEntry | null {
const [entry, setEntry] = useState<IntersectionObserverEntry | null>(null);
const optionsRef = useRef(options);
useEffect(() => {
if (typeof window === 'undefined' || !ref.current) return;
const observer = new IntersectionObserver(([newEntry]) => {
if (newEntry) setEntry(newEntry);
}, optionsRef.current);
observer.observe(ref.current);
return () => {
observer.disconnect();
};
}, [ref]);
import { type RefObject, useEffect, useState } from 'react';
export function useIntersectionObserver(
ref: RefObject<Element | null>,
options?: IntersectionObserverInit,
): IntersectionObserverEntry | null {
const [entry, setEntry] = useState<IntersectionObserverEntry | null>(null);
useEffect(() => {
if (typeof window === 'undefined' || !ref.current) return;
const observer = new IntersectionObserver(([newEntry]) => {
if (newEntry) setEntry(newEntry);
}, options);
observer.observe(ref.current);
return () => {
observer.disconnect();
};
}, [ref, options?.root, options?.rootMargin, options?.threshold]);

Copilot uses AI. Check for mistakes.
Comment on lines +7 to +12
const timeoutId = window.setTimeout(() => {
setDebouncedValue(value);
}, delay);

return () => {
window.clearTimeout(timeoutId);
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

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

This hook uses window.setTimeout/window.clearTimeout. That will throw in non-DOM runtimes (e.g. React Native, Node without JSDOM) and contradicts the README claim that non-DOM hooks have no DOM dependency. Prefer globalThis.setTimeout/globalThis.clearTimeout (or the unqualified setTimeout/clearTimeout) and guard if needed.

Suggested change
const timeoutId = window.setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
window.clearTimeout(timeoutId);
const timeoutId = globalThis.setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
globalThis.clearTimeout(timeoutId);

Copilot uses AI. Check for mistakes.
Comment on lines +13 to +18
const id = window.setInterval(() => {
callbackRef.current();
}, delay);

return () => {
window.clearInterval(id);
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

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

This hook uses window.setInterval/window.clearInterval, which will throw in non-DOM runtimes (and conflicts with the README claim that these hooks have no DOM dependency). Prefer globalThis.setInterval/globalThis.clearInterval (or unqualified setInterval/clearInterval) so the hook works anywhere effects can run.

Suggested change
const id = window.setInterval(() => {
callbackRef.current();
}, delay);
return () => {
window.clearInterval(id);
const id = globalThis.setInterval(() => {
callbackRef.current();
}, delay);
return () => {
globalThis.clearInterval(id);

Copilot uses AI. Check for mistakes.
Comment on lines +10 to +19
useEffect(() => {
if (delay === null) return;

const id = window.setTimeout(() => {
callbackRef.current();
}, delay);

return () => {
window.clearTimeout(id);
};
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

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

This hook uses window.setTimeout/window.clearTimeout, which will throw in non-DOM runtimes (and conflicts with the README claim that these hooks have no DOM dependency). Prefer globalThis.setTimeout/globalThis.clearTimeout (or unqualified timers) to keep the hook environment-agnostic.

Copilot uses AI. Check for mistakes.
Comment on lines +1 to 4
# @ciscode/hooks-kit

Template repository for building reusable React TypeScript **npm libraries**
(components + hooks + utilities).
12 production-ready React hooks. Zero runtime dependencies. SSR-safe.

Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

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

README states “12 production-ready React hooks”, but the public API currently also exports useNoop (13 total). Either add useNoop to the docs and adjust the count, or remove it from the public exports so the README matches what consumers can import.

Copilot uses AI. Check for mistakes.
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

npx lint-staged
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

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

This Husky hook file no longer has a shebang (and doesn’t source Husky’s helper). Git executes hooks as standalone scripts; without a shebang this commonly fails with “exec format error”. Restore the standard Husky hook header (e.g. #!/usr/bin/env sh + sourcing _/husky.sh) so npx lint-staged reliably runs cross-platform.

Copilot uses AI. Check for mistakes.
Comment on lines +151 to +152
-Dsonar.tests=test
-Dsonar.test.inclusions=**/*.spec.ts,**/*.test.ts
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

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

Sonar is configured with -Dsonar.tests=test, but this repo’s tests live under src/**/__tests__ (no test/ directory). This likely prevents Sonar from classifying any tests correctly and can skew coverage/new-code metrics. Point sonar.tests at the actual test locations (or omit it and rely on sonar.test.inclusions).

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

Copilot uses AI. Check for mistakes.
Comment on lines 141 to 146
- 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:
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

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

The SonarCloud job always runs and requires secrets.SONAR_TOKEN. On pull requests from forks, secrets are not available and this job will fail, blocking CI. If you expect external contributions, gate this step/job to only run when the token is available or when the PR originates from the same repo.

Copilot uses AI. Check for mistakes.
Comment on lines +23 to 54
- 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
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

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

Validate version tag and package.json only checks that the tag v${package.json version} exists somewhere in the repo, not that it points to the current commit being published. This can allow publishing from an untagged commit (or the wrong tag). Verify the tag is an exact match for HEAD (e.g., git describe --exact-match --tags HEAD) and that it equals v${PKG_VERSION}.

Copilot uses AI. Check for mistakes.
@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud bot commented Apr 7, 2026

@Zaiidmo Zaiidmo merged commit ce51c6f into master Apr 7, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants