Takazudo Modular Styleguide

Taxonomy/ArrowTag

Default
Large
Large Group
Large Linked
Size Comparison
Small
Small Group

Component Source

arrow-tag.tsx

/**
 * Size presets for arrow polygon clip-paths and spacing.
 * sm: compact tags for list items
 * lg: larger tags for article detail page headers
 */
import type { FunctionComponent, ComponentChildren } from 'preact';
const SIZE_STYLES = {
  sm: {
    borderClip: '[clip-path:polygon(0_0,calc(100%-6px)_0,100%_50%,calc(100%-6px)_100%,0_100%)]',
    contentClip:
      '[clip-path:polygon(1px_1px,calc(100%-7px)_1px,calc(100%-1px)_50%,calc(100%-7px)_calc(100%-1px),1px_calc(100%-1px))]',
    contentSize: 'text-[0.65rem] lg:text-xs pl-[6px] pr-[12px] pt-[.2em] pb-[.2em]',
  },
  lg: {
    borderClip: '[clip-path:polygon(0_0,calc(100%-16px)_0,100%_50%,calc(100%-16px)_100%,0_100%)]',
    contentClip:
      '[clip-path:polygon(1px_1px,calc(100%-17px)_1px,calc(100%-1px)_50%,calc(100%-17px)_calc(100%-1px),1px_calc(100%-1px))]',
    contentSize: 'text-sm lg:text-base pl-[8px] pr-[22px] pt-[.25em] pb-[.25em]',
  },
};

/**
 * Renders label text with parenthesized portions in smaller font.
 * Matches both full-width (...) and half-width (...) parentheses.
 */
function FormattedLabel({ label }: { label: string }) {
  const parts = label.split(/(([^)]*)|\([^)]*\))/);
  if (parts.length === 1) return <>{label}</>;
  return (
    <>
      {parts.map((part, i) =>
        /^[((]/.test(part) ? (
          <span key={i} className="text-[.7em]">
            {part}
          </span>
        ) : (
          <span key={i}>{part}</span>
        ),
      )}
    </>
  );
}

interface ArrowTagProps {
  label: string;
  size?: 'sm' | 'lg';
  suffix?: ComponentChildren;
  active?: boolean;
}

/**
 * Arrow polygon tag shape with bordered clip-path effect.
 * Renders the visual tag element — callers handle wrapper (li, a, etc.)
 *
 * Hover/active inversion is self-contained via group-hover/group-active.
 * Parent element must have `group` class for hover to work.
 */
export const ArrowTag: FunctionComponent<ArrowTagProps> = ({
  label,
  size = 'sm',
  suffix,
  active,
}) => {
  const styles = SIZE_STYLES[size];

  return (
    <span className="relative inline-flex">
      {/* Border layer */}
      <span
        className={`absolute inset-0 zd-tag-border ${active ? 'bg-zd-literal-black' : 'bg-zd-literal-white'} group-hover:bg-zd-literal-black group-active:!bg-zd-active ${styles.borderClip}`}
      />
      {/* Content layer with inset clip for border effect */}
      <span
        className={`relative inline-flex items-center font-thin no-underline text-shadow-none zd-tag-content ${active ? 'text-zd-literal-black bg-zd-literal-white' : 'text-zd-literal-white bg-zd-literal-black'} group-hover:text-zd-literal-black group-hover:bg-zd-literal-white group-active:!bg-zd-active ${styles.contentSize} ${styles.contentClip}`}
      >
        <span className="zd-hash">#</span>
        <span>
          <FormattedLabel label={label} />
          {suffix}
        </span>
      </span>
    </span>
  );
};

Story Source

arrow-tag.stories.tsx

import { ArrowTag } from './arrow-tag';
import type { ControlsMap } from '../../sub-packages/styleguide-v2/src/data/control-types';

/**
 * ArrowTag Component
 *
 * Arrow polygon tag shape with bordered clip-path effect.
 * Two size variants: sm (list items) and lg (detail page headers).
 * Callers handle wrapper elements (li, a, etc.)
 */
export const meta = {
  title: 'Taxonomy/ArrowTag',
};

export const controls: ControlsMap = {
  label: { type: 'text', default: 'Modular Synthesizer' },
  size: { type: 'select', default: 'sm', options: ['sm', 'lg'] },
};

/**
 * Default - controllable via props panel
 */
export const Default = {
  args: { label: 'Modular Synthesizer', size: 'sm' },
  render: ({ label, size }: { label: string; size: 'sm' | 'lg' }) => (
    <ArrowTag label={label} size={size} />
  ),
};

/**
 * Small size — used in article list items
 */
export const Small = () => <ArrowTag label="Modular Synthesizer" size="sm" />;

/**
 * Large size — used in article detail page headers
 */
export const Large = () => <ArrowTag label="Modular Synthesizer" size="lg" />;

/**
 * Small tags in a row — simulates list item tag display
 */
export const SmallGroup = () => (
  <ul className="inline">
    {['Modular Synthesizer', '電源', 'DIY', 'Case(ケース)'].map((label) => (
      <li key={label} className="inline-flex mr-[6px] mb-[4px] whitespace-nowrap">
        <ArrowTag label={label} size="sm" />
      </li>
    ))}
  </ul>
);

/**
 * Large tags in a row — simulates detail page header tag display
 */
export const LargeGroup = () => (
  <ul className="inline">
    {['Modular Synthesizer', '電源', 'DIY', 'Case(ケース)'].map((label) => (
      <li key={label} className="inline-flex mr-[8px] mb-[8px] whitespace-nowrap">
        <ArrowTag label={label} size="lg" />
      </li>
    ))}
  </ul>
);

/**
 * Large tags wrapped in links — simulates detail page with hover/active states
 */
export const LargeLinked = () => (
  <ul className="inline">
    {['Modular Synthesizer', '電源', 'DIY', 'Case(ケース)'].map((label) => (
      <li key={label} className="inline-flex mr-[8px] mb-[8px] whitespace-nowrap">
        <a className="group" href="#">
          <ArrowTag label={label} size="lg" />
        </a>
      </li>
    ))}
  </ul>
);

/**
 * Size comparison — sm vs lg side by side
 */
export const SizeComparison = () => (
  <div className="flex flex-col gap-vgap-md">
    <div>
      <div className="text-xs text-zd-gray pb-vgap-xs">size=&quot;sm&quot;</div>
      <ArrowTag label="Modular Synthesizer" size="sm" />
    </div>
    <div>
      <div className="text-xs text-zd-gray pb-vgap-xs">size=&quot;lg&quot;</div>
      <ArrowTag label="Modular Synthesizer" size="lg" />
    </div>
  </div>
);