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
4 changes: 2 additions & 2 deletions .github/workflows/pull_request.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Pull request workflow
name: npm test

on:
pull_request:
Expand All @@ -16,7 +16,7 @@ jobs:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '20.9.0'
node-version-file: '.nvmrc'
cache: 'yarn'
cache-dependency-path: '**/yarn.lock'
- name: Install dependencies
Expand Down
35 changes: 35 additions & 0 deletions .oxfmtrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"printWidth": 100,
"singleQuote": true,
"trailingComma": "es5",
"importOrder": [
".*styles.css$",
"",
"dayjs",
"^react$",
"^next$",
"^next/.*$",
"<BUILTIN_MODULES>",
"<THIRD_PARTY_MODULES>",
"^@mantine/(.*)$",
"^@mantinex/(.*)$",
"^@mantine-tests/(.*)$",
"^@docs/(.*)$",
"^@/.*$",
"^../(?!.*\\.css$).*$",
"^./(?!.*\\.css$).*$",
"\\.module\\.css$",
"(?<!\\.module)\\.css$"
],
"sortPackageJson": false,
"ignorePatterns": [
"*.d.ts",
"*.mdx",
"*.md",
"packages/*/*/styles.css",
"packages/*/*/styles.layer.css",
"packages/*/*/styles/*.css",
"docs/.next",
"docs/out"
Comment thread
gfazioli marked this conversation as resolved.
]
}
35 changes: 0 additions & 35 deletions .prettierrc.mjs

This file was deleted.

17 changes: 7 additions & 10 deletions .storybook/main.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,17 @@
import { dirname, join } from 'path';
import type { StorybookConfig } from '@storybook/react-vite';

function getAbsolutePath(value: string): any {
return dirname(require.resolve(join(value, 'package.json')));
}

const config: StorybookConfig = {
core: {
disableWhatsNewNotifications: true,
disableTelemetry: true,
enableCrashReports: false,
},
stories: ['../package/src/**/*.story.@(js|jsx|mjs|ts|tsx)'],
addons: [getAbsolutePath('@storybook/addon-essentials'), getAbsolutePath('storybook-dark-mode')],
addons: ['@storybook/addon-themes'],
framework: {
name: getAbsolutePath('@storybook/react-vite'),
name: '@storybook/react-vite',
options: {},
},
docs: {
autodocs: false,
},
};

export default config;
54 changes: 21 additions & 33 deletions .storybook/preview.tsx
Original file line number Diff line number Diff line change
@@ -1,41 +1,29 @@
import type { Preview } from '@storybook/react';

import '@mantine/core/styles.css';

import React, { useEffect } from 'react';
import { addons } from '@storybook/preview-api';
import { DARK_MODE_EVENT_NAME } from 'storybook-dark-mode';
import { MantineProvider, useMantineColorScheme } from '@mantine/core';

const channel = addons.getChannel();

function ColorSchemeWrapper({ children }: { children: React.ReactNode }) {
const { setColorScheme } = useMantineColorScheme();
const handleColorScheme = (value: boolean) => setColorScheme(value ? 'dark' : 'light');
import { MantineProvider } from '@mantine/core';

useEffect(() => {
channel.on(DARK_MODE_EVENT_NAME, handleColorScheme);
return () => channel.off(DARK_MODE_EVENT_NAME, handleColorScheme);
}, [channel]);

return <>{children}</>;
}

export const decorators = [
(renderStory: any) => <ColorSchemeWrapper>{renderStory()}</ColorSchemeWrapper>,
(renderStory: any) => <MantineProvider>{renderStory()}</MantineProvider>,
];
export const parameters = {
layout: 'padded',
};

const preview: Preview = {
parameters: {
actions: { argTypesRegex: '^on[A-Z].*' },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
export const globalTypes = {
theme: {
name: 'Theme',
description: 'Mantine color scheme',
defaultValue: 'light',
toolbar: {
icon: 'mirror',
items: [
{ value: 'light', title: 'Light' },
{ value: 'dark', title: 'Dark' },
],
},
},
};

export default preview;
export const decorators = [
(renderStory: any, context: any) => {
const scheme = (context.globals.theme || 'light') as 'light' | 'dark';
return <MantineProvider forceColorScheme={scheme}>{renderStory()}</MantineProvider>;
},
];
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
110 changes: 48 additions & 62 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,71 +1,51 @@
# CLAUDE.md

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

## What This Is

`@gfazioli/mantine-select-stepper` — a Mantine-based React component that lets users cycle through a list of options via stepper buttons (forward/backward navigation with keyboard support, infinite looping, disabled items, swipe gestures, vertical orientation, and animations). Published as an npm package under the `@gfazioli` scope.
## Project
`@gfazioli/mantine-select-stepper` — a Mantine-based React component that lets users cycle through a list of options via stepper buttons, with forward/backward navigation, keyboard support, infinite looping, disabled items, swipe gestures, vertical orientation, and animations.

## Commands

| Command | Purpose |
|---------|---------|
| `yarn build` | Build the npm package (Rollup → `package/dist/`) |
| `yarn dev` | Start docs dev server (Next.js, port 9281) |
| `yarn test` | Full test suite: syncpack → prettier → typecheck → lint → jest |
| `yarn jest` | Run only Jest tests |
| `yarn jest --testPathPattern=SelectStepper` | Run a single test file |
| `yarn docgen` | Generate component API docs (`docgen.json`) |
| `yarn docs:build` | Build docs site (runs docgen first) |
| `yarn docs:deploy` | Build + deploy docs to GitHub Pages |
| `yarn prettier:write` | Auto-fix formatting |
| `yarn clean` | Remove `package/dist/` |
| `yarn release:patch` | Bump patch version + deploy docs |
| `diny yolo` | AI-assisted auto-commit (stages all, generates message, commits) |

**Important:** After changing `package/src/`, always run `yarn clean && yarn build` before `yarn test` — the docs workspace imports from `package/dist/`, so typecheck will fail with stale types.
| `yarn build` | Build the npm package via Rollup |
| `yarn dev` | Start the Next.js docs dev server (port 9281) |
| `yarn test` | Full test suite (syncpack + oxfmt + typecheck + lint + jest) |
| `yarn jest` | Run only Jest unit tests |
| `yarn docgen` | Generate component API docs (docgen.json) |
| `yarn docs:build` | Build the Next.js docs site for production |
| `yarn docs:deploy` | Build and deploy docs to GitHub Pages |
| `yarn lint` | Run oxlint |
| `yarn format:write` | Format all files with oxfmt |
| `yarn storybook` | Start Storybook dev server |
| `yarn clean` | Remove build artifacts |
| `yarn release:patch` | Bump patch version and deploy docs |
| `diny yolo` | AI-assisted commit (stage all, generate message, commit + push) |

> **Important**: After changing the public API, always run `yarn clean && yarn build` before `yarn test`.

## Architecture

This is a **Yarn workspaces** monorepo with two workspaces:

- **`package/`** — the published npm package source
- **`docs/`** — Next.js documentation site (deployed to GitHub Pages)

### Package source (`package/src/`)
### Workspace Layout
Yarn workspaces monorepo with two workspaces: `package/` (npm package) and `docs/` (Next.js 15 documentation site).

### Package Source (`package/src/`)
Single component library with a flat structure:

- `SelectStepper.tsx` — main component, built with `polymorphicFactory` from Mantine
- `SelectStepper.module.css` — CSS Modules styles (scoped via `hash-css-selector` with `me` prefix)
- `SelectStepper.module.css` — CSS Modules styles
- `SelectStepper.test.tsx` — Jest tests covering navigation, keyboard, a11y, imperative API, callbacks, swipe, responsive
- `SelectStepper.story.tsx` — Storybook stories
- `SelectStepperMediaVariables.tsx` — CSS media queries for responsive props (InlineStyles pattern)
- `get-input-offsets/` — utility for calculating input wrapper margin offsets
- `index.ts` — public exports (component + all types + `ComboboxItem` + `StyleProp` re-export)

### Exported types

`SelectStepper`, `SelectStepperBaseProps`, `SelectStepperProps`, `SelectStepperFactory`, `SelectStepperCssVariables`, `SelectStepperStylesNames`, `SelectStepperVariant`, `SelectStepperItem`, `SelectStepperOrientation`, `SelectStepperRef`, `ComboboxItem`, `StyleProp`
Exported types: `SelectStepper`, `SelectStepperBaseProps`, `SelectStepperProps`, `SelectStepperFactory`, `SelectStepperCssVariables`, `SelectStepperStylesNames`, `SelectStepperVariant`, `SelectStepperItem`, `SelectStepperOrientation`, `SelectStepperRef`, `ComboboxItem`, `StyleProp`.

### Build pipeline
### Build Pipeline
Rollup bundles to dual ESM/CJS with `'use client'` banner. CSS modules hashed with `hash-css-selector` (prefix `me`). TypeScript declarations via `rollup-plugin-dts`. CSS split into `styles.css` and `styles.layer.css`.

Rollup (`rollup.config.mjs`) produces dual output:
- ESM → `package/dist/esm/`
- CJS → `package/dist/cjs/`
- Types → `package/dist/types/` (generated by `scripts/generate-dts`)
- CSS → `package/dist/styles.css` (PostCSS with hashed selectors)

Non-index chunks are automatically prefixed with `'use client'` directive.

### Docs (`docs/`)

- `docs/pages/` — Next.js pages (only `index.tsx` as main page)
- `docs/demos/` — interactive demo components (`SelectStepper.demo.*.tsx`), each showcasing a specific feature
- `docs/components/` — shared layout (Shell, Footer, Logo)

### Mantine patterns used
## Component Details

### Mantine Patterns Used
- `polymorphicFactory` for component creation (supports `component` prop)
- `useProps` with `defaultProps` for prop resolution
- `createVarsResolver` for CSS variable mapping (non-responsive props only)
Expand All @@ -76,24 +56,30 @@ Non-index chunks are automatically prefixed with `'use client'` directive.
- `InlineStyles` + `useRandomClassName` for responsive CSS media queries (`viewWidth`, `viewHeight`, `size`)
- `useMatches` for JS-consumed responsive props (`orientation`)

### Key implementation details
### Responsive Props (CSS-native)
`SelectStepperMediaVariables` generates `<style>` with `@media` queries for `viewWidth`, `viewHeight`, `size` — zero React re-renders on resize. Uses concrete pixel values for `--ai-size` (NOT `var(--ai-size-sm)` references which are scoped to ActionIcon's CSS module). Override passed via `getStyles('leftSection', { style: actionSizeStyle })` to merge with computed styles.

### Responsive Orientation (JS)
`useMatches` resolves `orientation` because it controls React component structure (Stack/Group), keyboard keys, and icon direction — cannot be expressed as CSS.

### Internal vs External Navigation
`isInternalNavRef` distinguishes navigation-triggered value changes from external controlled mode changes, preventing the sync `useEffect` from canceling the animation timeout.

- **Responsive props (CSS-native):** `SelectStepperMediaVariables` generates `<style>` with `@media` queries for `viewWidth`, `viewHeight`, `size` — zero React re-renders on resize. Uses concrete pixel values for `--ai-size` (NOT `var(--ai-size-sm)` references which are scoped to ActionIcon's CSS module). Override passed via `getStyles('leftSection', { style: actionSizeStyle })` to merge with computed styles.
- **Responsive orientation (JS):** `useMatches` resolves `orientation` because it controls React component structure (Stack/Group), keyboard keys, and icon direction — cannot be expressed as CSS.
- **Internal vs external navigation flag:** `isInternalNavRef` distinguishes navigation-triggered value changes from external controlled mode changes, preventing the sync `useEffect` from canceling the animation timeout
- **Pointer events for swipe:** Uses `PointerDown/PointerUp` with threshold detection, no external dependencies
- **Vertical mode:** Switches `Group` → `Stack`, `translateX` → `translateY`, and remaps keyboard arrow keys
- **Narrow container handling:** `.view` uses `min-width: 0` to allow flex shrinking; `.content` uses `width: 100%` (not fixed CSS var) so items match the actual view width and `translateX(-100%)` stays aligned
### Pointer Events for Swipe
Uses `PointerDown/PointerUp` with threshold detection, no external dependencies.

## Tech Stack
### Vertical Mode
Switches `Group` to `Stack`, `translateX` to `translateY`, and remaps keyboard arrow keys.

- **Mantine 8.x**, **React 19**, **TypeScript 5.9**
- **Next.js 15** (docs)
- **Yarn 4** (package manager)
- **Jest** + `@mantine-tests/core` + `@testing-library/react` (testing)
- **Rollup** (build), **ESBuild** (transpilation)
- **Storybook 8** (development)
### Narrow Container Handling
`.view` uses `min-width: 0` to allow flex shrinking; `.content` uses `width: 100%` (not fixed CSS var) so items match the actual view width and `translateX(-100%)` stays aligned.

## Peer Dependencies
## Testing
Jest with `jsdom`, `esbuild-jest` transform, CSS mocked via `identity-obj-proxy`. Tests use `@mantine-tests/core` render helper.

The package requires: `@mantine/core`, `@mantine/hooks`, `@tabler/icons-react`, `react`, `react-dom`.
## Ecosystem
This repo is part of the Mantine Extensions ecosystem, derived from the `mantine-base-component` template. See the workspace `CLAUDE.md` (in the parent directory) for:
- Development checklist (code -> test -> build -> docs -> release)
- Cross-cutting patterns (compound components, responsive CSS, GitHub sync)
- Update packages workflow
- Release process
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
## Overview

This component is created on top of the [Mantine](https://mantine.dev/) library.
It requires **Mantine 9.x** and **React 19**.

The [Mantine SelectStepper](https://gfazioli.github.io/mantine-select-stepper/) is a Mantine-based React component that provides an elegant way to select from a list of options through a stepper interface. Users can navigate forward and backward through items using action buttons, with support for keyboard navigation, infinite looping, disabled items, and smooth animations. Built with TypeScript and fully integrated with Mantine's Styles API, it offers extensive customization options including custom icons, animation timing, viewport width, and border styling.

Expand Down
19 changes: 9 additions & 10 deletions docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,9 @@
},
"dependencies": {
"@gfazioli/mantine-select-stepper": "workspace:*",
"@mantine/code-highlight": "8.3.15",
"@mantine/core": "8.3.15",
"@mantine/dates": "8.3.15",
"@mantine/hooks": "8.3.15",
"@mantine/code-highlight": "9.0.0",
"@mantine/core": "9.0.0",
"@mantine/hooks": "9.0.0",
"@mantinex/demo": "^2.0.0",
"@mantinex/dev-icons": "^2.0.0",
"@mantinex/mantine-header": "^2.0.0",
Expand All @@ -22,21 +21,21 @@
"@mantinex/shiki": "^1.1.0",
"@mdx-js/loader": "^3.1.1",
"@mdx-js/react": "^3.1.1",
"@next/mdx": "^15.5.12",
"@tabler/icons-react": "^3.36.1",
"@next/mdx": "^15.5.14",
"@tabler/icons-react": "^3.41.1",
"@types/mdx": "^2.0.13",
"dayjs": "^1.11.19",
"next": "15.5.12",
"next": "15.5.14",
"react": "19.2.4",
"react-dom": "19.2.4",
"remark-slug": "^7.0.1",
"shiki": "^3.22.0",
"shiki": "^3.23.0",
"type-fest": "^4.41.0"
},
"devDependencies": {
"@types/node": "^22.19.11",
"@types/node": "^22.19.17",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"typescript": "5.9.3"
"typescript": "6.0.2"
}
}
1 change: 0 additions & 1 deletion docs/pages/_app.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import '@mantine/core/styles.css';
// Core
import '@mantine/code-highlight/styles.css';
import '@mantine/dates/styles.css';
import '@mantinex/demo/styles.css';
import '@mantinex/mantine-header/styles.css';
import '@mantinex/mantine-logo/styles.css';
Expand Down
Loading
Loading