Skip to content
Merged
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
942 changes: 0 additions & 942 deletions .yarn/releases/yarn-4.12.0.cjs

This file was deleted.

940 changes: 940 additions & 0 deletions .yarn/releases/yarn-4.13.0.cjs

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion .yarnrc.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
nodeLinker: node-modules

yarnPath: .yarn/releases/yarn-4.12.0.cjs
yarnPath: .yarn/releases/yarn-4.13.0.cjs
58 changes: 58 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## What This Is

`@gfazioli/mantine-clock` β€” an analog Clock component and time-management hooks (`useClock`, `useClockCountDown`) for React/Mantine. Supports live ticking, static display, timezone conversion (via dayjs), sector arcs, and extensive styling via Mantine's Styles API.

## Commands

| Command | Purpose |
|---------|---------|
| `yarn build` | Build the npm package (Rollup β†’ `package/dist/`) |
| `yarn dev` | Start Next.js docs dev server (port 9281) |
| `yarn test` | Full suite: syncpack + prettier + typecheck + lint + jest |
| `yarn jest` | Run Jest tests only |
| `yarn jest --testPathPattern=Clock.test` | Run a single test file |
| `yarn docgen` | Generate component API docs (docgen.json) |
| `yarn eslint` | ESLint (cached) |
| `yarn stylelint` | Stylelint for CSS (cached) |
| `yarn prettier:check` | Check formatting |
| `yarn prettier:write` | Fix formatting |
| `yarn typecheck` | TypeScript check (root + docs) |
| `yarn clean && yarn build` | Clean rebuild |
| `yarn docs:deploy` | Build docs + deploy to GitHub Pages |
| `yarn release:patch` | Bump patch version + deploy docs |
| `diny yolo` | AI-assisted auto-commit (stages all, generates message, commits) |

## Architecture

This is a Yarn workspaces monorepo with two workspaces: `package/` (the npm library) and `docs/` (Next.js documentation site).

### Package (`package/src/`)

- **`Clock.tsx`** β€” Main component. Uses Mantine's `factory()` pattern with `createVarsResolver` for CSS variable theming. Internally splits into an SSR-safe shell (renders static markup before mount) and a `RealClock` sub-component that handles live hand rotation via `setInterval`. Time parsing supports strings (`"HH:MM:SS"`), `Date`, and dayjs objects. Timezone conversion uses `dayjs/plugin/timezone`. Arc sectors are rendered as SVG paths.
- **`Clock.module.css`** β€” CSS Modules styles, scoped with `hash-css-selector` (prefix `me`).
- **`hooks/use-clock.ts`** β€” `useClock` hook: returns formatted time data (hours, minutes, seconds, amPm, day, week, etc.) with configurable timezone, 24h format, padding, and update frequency.
- **`hooks/use-clock-count-down.ts`** β€” `useClockCountDown` hook: countdown timer supporting both target dates and duration-based countdowns, with start/pause/resume/reset controls.
- **`index.ts`** β€” Public API barrel file. All exports go through here.

### Build Pipeline

Rollup (`rollup.config.mjs`) produces ESM (`.mjs`) and CJS (`.cjs`) output with source maps. CSS Modules are extracted and minimized via `rollup-plugin-postcss`. Non-index chunks get a `'use client'` banner. Types are generated separately via `scripts/generate-dts`.

### Docs (`docs/`)

Next.js 15 site deployed to GitHub Pages. Demo components live in `docs/demos/`. Shared shell/footer components in `docs/components/`.

## Key Patterns

- **Mantine Factory Pattern**: The Clock component uses `factory<ClockFactory>()` with `useProps`, `useStyles`, and `createVarsResolver` β€” follow this same pattern for any new sub-components.
- **SSR Hydration Safety**: The component renders a static shell (`!hasMounted`) to prevent hydration mismatches, then switches to live rendering after mount.
- **CSS Variables**: All visual properties are exposed as `--clock-*` CSS custom properties resolved through `varsResolver`. Color props go through `parseThemeColor()`.
- **dayjs for Timezone**: Both the component and hooks use dayjs with `utc` and `timezone` plugins β€” never use raw `Date` for timezone operations.

## Testing

Tests use Jest + `@testing-library/react` + `@mantine-tests/core`. Test files are co-located with source (`*.test.tsx`). The `jest-environment-jsdom` environment is used. Storybook stories (`*.story.tsx`) are also co-located for visual development.
29 changes: 29 additions & 0 deletions docs/demos/Clock.demo.configurator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,5 +78,34 @@ export const configurator: MantineDemo = {
max: 1,
step: 0.01,
},
// Shape Settings
{
type: 'select',
prop: 'shape',
initialValue: 'circle',
libraryValue: 'circle',
data: [
{ label: 'Circle', value: 'circle' },
{ label: 'Rounded Rectangle', value: 'rounded-rect' },
],
},
{
type: 'number',
prop: 'borderRadius',
initialValue: 40,
libraryValue: 40,
min: 0,
max: 100,
step: 1,
},
{
type: 'number',
prop: 'aspectRatio',
initialValue: 1,
libraryValue: 1,
min: 0.6,
max: 1.5,
step: 0.1,
},
],
};
92 changes: 92 additions & 0 deletions docs/demos/Clock.demo.customHands.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { Clock } from '@gfazioli/mantine-clock';
import { Group } from '@mantine/core';
import { MantineDemo } from '@mantinex/demo';

const code = `
import { Clock } from '@gfazioli/mantine-clock';

function Demo() {
return (
<Group justify="center">
<Clock
size={350}
renderSecondHand={({ angle, length, centerX, centerY }) => (
<svg
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
zIndex: 17,
pointerEvents: 'none',
}}
>
<line
x1={centerX}
y1={centerY}
x2={centerX + Math.sin((angle * Math.PI) / 180) * length}
y2={centerY - Math.cos((angle * Math.PI) / 180) * length}
stroke="var(--mantine-color-red-6)"
strokeWidth={2}
strokeLinecap="round"
/>
<circle
cx={centerX + Math.sin((angle * Math.PI) / 180) * (length - 10)}
cy={centerY - Math.cos((angle * Math.PI) / 180) * (length - 10)}
r={4}
fill="var(--mantine-color-red-6)"
/>
</svg>
)}
/>
</Group>
);
}
`;

function Demo() {
return (
<Group justify="center">
<Clock
size={350}
renderSecondHand={({ angle, length, centerX, centerY }) => (
<svg
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
zIndex: 17,
pointerEvents: 'none',
}}
>
<line
x1={centerX}
y1={centerY}
x2={centerX + Math.sin((angle * Math.PI) / 180) * length}
y2={centerY - Math.cos((angle * Math.PI) / 180) * length}
stroke="var(--mantine-color-red-6)"
strokeWidth={2}
strokeLinecap="round"
/>
<circle
cx={centerX + Math.sin((angle * Math.PI) / 180) * (length - 10)}
cy={centerY - Math.cos((angle * Math.PI) / 180) * (length - 10)}
r={4}
fill="var(--mantine-color-red-6)"
/>
</svg>
)}
/>
</Group>
);
}

export const customHands: MantineDemo = {
type: 'code',
component: Demo,
code,
defaultExpanded: false,
};
73 changes: 73 additions & 0 deletions docs/demos/Clock.demo.digital.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { Clock } from '@gfazioli/mantine-clock';
import { Group, Stack, Text } from '@mantine/core';
import { MantineDemo } from '@mantinex/demo';

const code = `
import { Clock } from '@gfazioli/mantine-clock';

function Demo() {
return (
<Stack gap="xl" align="center">
{/* Default digital clock */}
<Clock.Digital size="xl" timezone="UTC" />

{/* 12-hour format with AM/PM */}
<Clock.Digital
size="lg"
timezone="Europe/Rome"
use24Hours={false}
showAmPm
/>

{/* Without seconds, with date */}
<Clock.Digital
size="md"
showSeconds={false}
showDate
color="blue"
/>

{/* Custom separator */}
<Clock.Digital size="sm" separator=" : " />
</Stack>
);
}
`;

function Demo() {
return (
<Stack gap="xl" align="center">
<Group gap="xs">
<Text size="sm" c="dimmed" w={100}>
XL / UTC:
</Text>
<Clock.Digital size="xl" timezone="UTC" />
</Group>
<Group gap="xs">
<Text size="sm" c="dimmed" w={100}>
12h / Rome:
</Text>
<Clock.Digital size="lg" timezone="Europe/Rome" use24Hours={false} showAmPm />
</Group>
<Group gap="xs">
<Text size="sm" c="dimmed" w={100}>
No seconds:
</Text>
<Clock.Digital size="md" showSeconds={false} showDate color="blue" />
</Group>
<Group gap="xs">
<Text size="sm" c="dimmed" w={100}>
Custom sep:
</Text>
<Clock.Digital size="sm" separator=" : " />
</Group>
</Stack>
);
}

export const digital: MantineDemo = {
type: 'code',
component: Demo,
code,
defaultExpanded: false,
};
45 changes: 45 additions & 0 deletions docs/demos/Clock.demo.faceContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { Clock } from '@gfazioli/mantine-clock';
import { Group, Text } from '@mantine/core';
import { MantineDemo } from '@mantinex/demo';

const code = `
import { Clock } from '@gfazioli/mantine-clock';
import { Group, Text } from '@mantine/core';

function Demo() {
return (
<Group justify="center">
<Clock
size={300}
faceContent={
<Text size="xs" c="dimmed" style={{ marginTop: 80 }}>
SWISS MADE
</Text>
}
/>
</Group>
);
}
`;

function Demo() {
return (
<Group justify="center">
<Clock
size={300}
faceContent={
<Text size="xs" c="dimmed" style={{ marginTop: 80 }}>
SWISS MADE
</Text>
}
/>
</Group>
);
}

export const faceContentDemo: MantineDemo = {
type: 'code',
component: Demo,
code,
defaultExpanded: false,
};
48 changes: 48 additions & 0 deletions docs/demos/Clock.demo.mountAnimation.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { useState } from 'react';
import { Clock } from '@gfazioli/mantine-clock';
import { Button, Stack } from '@mantine/core';
import { MantineDemo } from '@mantinex/demo';

const code = `
import { useState } from 'react';
import { Clock } from '@gfazioli/mantine-clock';
import { Button, Group } from '@mantine/core';

function Demo() {
const [key, setKey] = useState(0);

return (
<Stack align="center" gap="md">
<Clock
key={key}
size={300}
animateOnMount
animateOnMountDuration={1500}
/>
<Button onClick={() => setKey((k) => k + 1)} variant="light">
Remount Clock
</Button>
</Stack>
);
}
`;

function Demo() {
const [key, setKey] = useState(0);

return (
<Stack align="center" gap="md">
<Clock key={key} size={300} animateOnMount animateOnMountDuration={1500} />
<Button onClick={() => setKey((k) => k + 1)} variant="light">
Remount Clock
</Button>
</Stack>
);
}

export const mountAnimation: MantineDemo = {
type: 'code',
component: Demo,
code,
defaultExpanded: false,
};
Loading
Loading