Shared/ClearFilterButton
Custom Label
Default
Disabled
In Filter Row
Interactive
Responsive Label
clear-filter-button.tsx
/**
* Shared presentational "clear filters" button — a bordered text button with
* a leading close-X icon (the "✕ 条件をクリア" look), replacing each filter
* UI's own hand-rolled "Clear All" / "すべてクリア" text button. Extracted for
* the filter-UI unification (issue #1334, epic #1333) so `/products/` and
* `/metamodule-ref/` share one look. Close-X path sourced from the dialog
* close button in `src/components/metamodule-ref/module-dialog.tsx`.
*
* Purely presentational — holds NO internal state. Callers own the
* clear-filters logic and pass it in via `onClick` / `disabled`.
*/
export interface ClearFilterButtonProps {
label?: string;
/**
* Optional shorter label shown below the `lg` breakpoint, paired with
* `label` as the `lg`+ wide variant (responsive span pair, matches the
* products `FilterToggle`'s 絞り込み/タグやキーワードで絞り込み narrow/wide
* pattern — #1375 fix5 / issue #1389). When omitted, `label` renders
* unconditionally at every width — the mm-ref consumers (`filter-panel.tsx`,
* `metamodule-ref-island.tsx`) rely on this default and must NOT change.
*/
labelNarrow?: string;
onClick?: () => void;
disabled?: boolean;
className?: string;
// data-* passthrough (e.g. `data-mm-filter-clear`) so callers can hang a
// DOM-contract hook off the rendered <button> without the component
// needing to know about it.
[key: `data-${string}`]: unknown;
}
const BASE_CLASS =
'font-futura text-sm whitespace-nowrap px-hgap-sm py-vgap-2xs border transition-colors ' +
'shrink-0 border-zd-gray text-zd-gray hover:text-zd-white hover:border-zd-white cursor-pointer ' +
'disabled:border-zd-gray/30 disabled:text-zd-gray/30 disabled:cursor-default';
export function ClearFilterButton({
label = '条件をクリア',
labelNarrow,
onClick,
disabled,
className,
...rest
}: ClearFilterButtonProps) {
return (
<button
type="button"
{...rest}
onClick={onClick}
disabled={disabled}
class={className ? `${BASE_CLASS} ${className}` : BASE_CLASS}
>
<svg
class="inline-block h-[1em] w-[1em] mr-[.35em] align-[-0.125em]"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
/>
</svg>
{labelNarrow !== undefined ? (
<>
<span class="lg:hidden">{labelNarrow}</span>
<span class="hidden lg:inline">{label}</span>
</>
) : (
label
)}
</button>
);
}
export default ClearFilterButton;
clear-filter-button.stories.tsx
import { useState } from 'preact/hooks';
import { ClearFilterButton } from './clear-filter-button';
import type { ControlsMap } from '@/sub-packages/styleguide-v2/src/data/control-types';
/**
* ClearFilterButton Component
*
* Shared presentational "clear filters" button — a bordered text button with
* a leading close-X icon (the "✕ 条件をクリア" look), replacing each filter
* UI's own hand-rolled "Clear All" / "すべてクリア" text button. Extracted for
* the filter-UI unification (issue #1334, epic #1333) so `/products/` and
* `/metamodule-ref/` share one look.
*
* Purely presentational — holds NO internal state. Callers own the
* clear-filters logic and pass it in via `onClick` / `disabled`.
*/
export const meta = {
title: 'Shared/ClearFilterButton',
};
export const controls: ControlsMap = {
label: { type: 'text', default: '条件をクリア' },
disabled: { type: 'boolean', default: false },
};
/**
* Default - controllable via props panel
*/
export const Default = {
args: { label: '条件をクリア', disabled: false },
render: ({ label, disabled }: { label: string; disabled: boolean }) => (
<ClearFilterButton label={label} disabled={disabled} />
),
};
/**
* Disabled — the SSR no-JS baseline before an island hydrates and enables it
* (the `/metamodule-ref/` pattern — see `filter-panel.tsx`)
*/
export const Disabled = () => <ClearFilterButton disabled />;
/**
* Custom label text
*/
export const CustomLabel = () => <ClearFilterButton label="すべてクリア" />;
/**
* Responsive narrow/wide label pair (#1375 fix5 / issue #1389) — `クリア`
* below `lg`, `条件をクリア` at `lg`+. Used by `/notes/` and `/products/`;
* resize the Astrobook viewport past `lg` (1024px) to see the switch. The
* mm-ref consumers (`filter-panel.tsx`, `metamodule-ref-island.tsx`) never
* pass `labelNarrow`, so they keep the unconditional single-label default
* seen in the stories above.
*/
export const ResponsiveLabel = () => (
<ClearFilterButton label="条件をクリア" labelNarrow="クリア" />
);
/**
* Right-aligned alongside a search input — mirrors the real
* `/metamodule-ref/` col1 row (`filter-panel.tsx`)
*/
export const InFilterRow = () => (
<div class="flex flex-col gap-vgap-sm sm:flex-row sm:items-center sm:gap-hgap-md w-[420px]">
<input
type="search"
placeholder="キーワードでフィルタ"
class="w-full sm:w-[240px] rounded-none border border-zd-white bg-white px-hgap-sm py-vgap-2xs text-base text-zd-literal-black placeholder:text-zd-subtext"
/>
<ClearFilterButton className="sm:ml-auto" />
</div>
);
/**
* Interactive — click toggles a counter so the click handler is visibly wired
*/
const InteractiveClear = () => {
const [clearCount, setClearCount] = useState(0);
return (
<div class="flex items-center gap-hgap-sm">
<ClearFilterButton onClick={() => setClearCount((v) => v + 1)} />
<p class="font-futura text-xs text-zd-subtext">Cleared: {clearCount}</p>
</div>
);
};
export const Interactive = () => <InteractiveClear />;