Takazudo Modular Styleguide

Shared/ClearFilterButton

Custom Label
Default
Disabled
In Filter Row
Interactive
Responsive Label

Component Source

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;

Story Source

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 />;