The frontend component of PuffPastry, meant to interface with the backend, which in turn interfaces with the smart contract.
選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。
 
 
 
 
 

18 KiB

sessionId
session-261002-165538-pjxq

Requirements

Overview & Goals

The objective of this initiative is to replace the fragmented, ad-hoc SVG icon implementations in src/components/icons/ with a high-utility, predictable, and performant Vue 3 icon architecture.

Currently, icons in the project are implemented with substantial inconsistencies:

  • Several icons wrap raw SVG markup inside container <div> tags with hardcoded layout properties (e.g., class="h-4 w-4 cursor-pointer mx-2").
  • Other icons render raw SVG tags with inconsistent prop contracts (e.g., :box="96" vs :viewBox="36" vs :width="20" vs hardcoded inline styles).
  • Fills, strokes, and hover behaviors are frequently hardcoded in scoped CSS or inline SVG attributes, preventing utility classes (Tailwind CSS text-*, fill-current, stroke-current, size-*) from controlling colors and dimensions naturally.

This proposal establishes a standardized, tree-shakeable, and type-safe icon system designed to maximize developer throughput, eliminate styling friction, reduce bundle waste, and ensure complete accessibility compliance.

Scope

In Scope

  • Foundation Layer: Create an extensible, typed IconBase.vue component and types.ts defining standard icon contracts (IconName, IconSize, sizing scales, accessibility attributes).
  • Icon Normalization: Refactor all 14 existing icon components (Abstain, Arrow, Cancel, Confirm, DownArrow, Download, Filter, Identification, Menu, Plus, SpeechBubbles, Stack, ThumbsDown, ThumbsUp) into pure SVG components using currentColor and consistent viewBox geometries.
  • Dynamic Registry: Create a unified AppIcon.vue for dynamic runtime icon rendering and src/components/icons/index.ts for clean tree-shakeable named exports.
  • Call-Site Migration: Refactor all 8 consuming components and views (Sidebar.vue, ProposalContainer.vue, ProposalDetail.vue, ProposalExportCard.vue, ProposalVotingCard.vue, VotingControls.vue, AmendmentCarousel.vue, HomeView.vue) to use the new icon interfaces and standard Tailwind classes.
  • Build & Quality Assurance: Ensure zero TypeScript errors (vue-tsc), clean Vite builds, and consistent rendering across light/dark backgrounds and responsive viewport sizes.

Out of Scope

  • Introducing external heavyweight icon packages or third-party web font libraries that would increase bundle size or external network dependencies.
  • Modifying non-icon business logic, wallet integrations, or backend API service definitions.

User Stories

  • As a Developer, I want all icons to behave as first-class Vue components with uniform props and currentColor support so that I can style their size, color, and hover states directly using Tailwind CSS without fighting inner scoped CSS or arbitrary wrapper <div>s.
  • As a Developer, I want autocompletion and type checking when specifying icon names so that runtime icon typos and missing assets are caught at compile time.
  • As an End User, I want all icons to render crisply, scale smoothly across devices, and support screen readers properly so that UI interactions are fast, intuitive, and accessible.

Functional Requirements

  • FR-1: Standardized Root Element: Every individual icon component must render a semantic <svg> root element without superfluous wrapper <div> elements.
  • FR-2: Sizing Flexibility: Icons must accept standardized size presets ('xs' | 'sm' | 'md' | 'lg' | 'xl'), numerical/pixel dimensions, or inherit sizing directly from standard Tailwind classes (size-4, w-5 h-5).
  • FR-3: Color & Theme Inheritance: Icons must inherit foreground colors from their parent container by default via fill="currentColor" or stroke="currentColor", allowing instant theme adaptations (text-slate-500, hover:text-green-500, fill-amber-500).
  • FR-4: Dynamic & Direct Imports: The system must support direct named component imports (e.g., import { IconFilter } from '@/components/icons') and dynamic component resolution (e.g., <AppIcon name="filter" />).
  • FR-5: Accessibility Support: Decorative icons must default to aria-hidden="true", while standalone interactive or informational icons must support customizable title and role="img" attributes.

Non-Functional Requirements

  • Performance: Zero runtime overhead from external bundle bloat; SVG trees must remain tree-shakeable and lightweight.
  • Type Safety: Fully typed with TypeScript 5+ and Vue 3 <script setup> compiler macros.
  • Maintainability: Clear separation of concern between icon vector geometries and presentation styles. Adding a new icon must require minimal boilerplate.

Technical Design

Current Implementation

The current codebase contains 14 icon files located in src/components/icons/:

  • DownArrow.vue, Filter.vue, Identification.vue, Menu.vue, Plus.vue, and SpeechBubbles.vue contain outer <div class="h-4 w-4 cursor-pointer mx-2"> wrappers, hardcoding margins, cursor behavior, and dimensions inside the icon definition.
  • Abstain.vue, Cancel.vue, and Confirm.vue define custom withDefaults(defineProps<{ box?: number; width?: number | string; height?: number | string }>()) props but hardcode geometry coordinates.
  • Stack.vue defines :viewBox="0 0 ${viewBox} ${viewBox}" (defaulting to 90) but hardcodes style="width: 20px; height: 20px;" and fill="white".
  • Arrow.vue hardcodes style="width: 10px; height: 20px;".
  • SpeechBubbles.vue and DownArrow.vue include scoped CSS with ID selectors and !important color overrides (#down-arrow:hover, #speech-bubbles:hover), preventing Tailwind styling from parent components.
  • Consuming components (VotingControls.vue, ProposalVotingCard.vue, ProposalExportCard.vue, AmendmentCarousel.vue, Sidebar.vue, ProposalContainer.vue, ProposalDetail.vue, HomeView.vue) use incompatible styling workarounds (e.g., manual :box="96", inline style overrides, or duplicate Tailwind wrapper classes).

Key Decisions

  1. First-Class Vue SVG Components with IconBase Wrapper:
    • Decision: Standardize each icon as a dedicated Vue component composed around a lightweight IconBase.vue (or direct standardized <svg> element) using currentColor.
    • Rationale: Eliminates unwanted DOM nesting (<div> wrapping <svg>), allows full Tailwind utility class passthrough (class="..." directly targets <svg>), and ensures maximum rendering performance and tree-shakability without external package bloat.
  2. Unified AppIcon.vue Dynamic Component:
    • Decision: Provide an AppIcon.vue component that maps an IconName string union to the corresponding icon component.
    • Rationale: Gives developers the choice between static named component imports (best for explicit, statically analyzable components) and dynamic string-based icon rendering (best for config-driven UI, tables, or iteration).
  3. Tailwind-First Sizing & Color Design:
    • Decision: Default icon dimensions to 1em (or preset sizes) and currentColor fill/stroke, allowing standard Tailwind classes (size-4, text-slate-400, hover:text-amber-500) to govern appearance effortlessly.
    • Rationale: Maximizes developer utility by keeping icon styling consistent with all other UI components in Tailwind CSS.

Proposed Architecture & Flow

graph TD
    subgraph Consumers
        A[Sidebar.vue]
        B[VotingControls.vue]
        C[ProposalVotingCard.vue]
        D[AmendmentCarousel.vue]
        E[ProposalContainer.vue]
        F[Other Views / Modals]
    end

    subgraph Icon System Layer
        G[AppIcon.vue Dynamic Component]
        H[Named Exports in icons/index.ts]
        I[IconBase.vue Core Sizing & A11y Wrapper]
    end

    subgraph Icon Definitions
        J[IconAbstain.vue]
        K[IconConfirm.vue]
        L[IconCancel.vue]
        M[IconFilter.vue]
        N[IconDownload.vue]
        O[IconIdentification.vue]
        P[Other 8 SVG Icons...]
    end

    A & B & C & D & E & F -->|Static Named Import| H
    A & B & C & D & E & F -->|Dynamic Name Prop| G
    G --> H
    H --> J & K & L & M & N & O & P
    J & K & L & M & N & O & P -->|Composes / Inherits| I

Data Models / Contracts

src/components/icons/types.ts

export type IconName =
  | 'abstain'
  | 'arrow'
  | 'cancel'
  | 'confirm'
  | 'down-arrow'
  | 'download'
  | 'filter'
  | 'identification'
  | 'menu'
  | 'plus'
  | 'speech-bubbles'
  | 'stack'
  | 'thumbs-down'
  | 'thumbs-up';

export type IconSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | number | string;

export interface IconBaseProps {
  size?: IconSize;
  viewBox?: string;
  title?: string;
  ariaHidden?: boolean;
  role?: string;
}

src/components/icons/IconBase.vue

<template>
  <svg
    xmlns="http://www.w3.org/2000/svg"
    :viewBox="viewBox"
    :width="computedSize"
    :height="computedSize"
    :aria-hidden="title ? undefined : ariaHidden"
    :aria-label="title"
    :role="title ? 'img' : (role || 'presentation')"
    class="inline-block shrink-0 align-middle select-none fill-current"
  >
    <title v-if="title">{{ title }}</title>
    <slot />
  </svg>
</template>

<script setup lang="ts">
import { computed } from 'vue';
import type { IconBaseProps } from './types';

const props = withDefaults(defineProps<IconBaseProps>(), {
  viewBox: '0 0 24 24',
  ariaHidden: true,
  size: 'md',
});

const sizeMap: Record<string, string> = {
  xs: '12',
  sm: '16',
  md: '20',
  lg: '24',
  xl: '32',
};

const computedSize = computed(() => {
  if (typeof props.size === 'number') return props.size;
  return sizeMap[props.size] || props.size;
});
</script>

File Structure & Changes

src/
├── components/
│   ├── icons/
│   │   ├── types.ts                   # [NEW] IconName, IconSize, Prop interfaces
│   │   ├── IconBase.vue               # [NEW] Common SVG wrapper component
│   │   ├── AppIcon.vue                # [NEW] Dynamic icon component resolver
│   │   ├── index.ts                   # [NEW] Centralized export entry point
│   │   ├── Abstain.vue                # [REFACTORED] Standardized SVG component
│   │   ├── Arrow.vue                  # [REFACTORED] Standardized SVG component
│   │   ├── Cancel.vue                 # [REFACTORED] Standardized SVG component
│   │   ├── Confirm.vue                # [REFACTORED] Standardized SVG component
│   │   ├── DownArrow.vue              # [REFACTORED] Removed wrapper div & CSS overrides
│   │   ├── Download.vue               # [REFACTORED] Standardized SVG component
│   │   ├── Filter.vue                 # [REFACTORED] Removed wrapper div & CSS overrides
│   │   ├── Identification.vue         # [REFACTORED] Removed wrapper div & standardized viewBox
│   │   ├── Menu.vue                   # [REFACTORED] Removed wrapper div & standardized stroke/fill
│   │   ├── Plus.vue                   # [REFACTORED] Removed wrapper div
│   │   ├── SpeechBubbles.vue          # [REFACTORED] Removed wrapper div & CSS overrides
│   │   ├── Stack.vue                  # [REFACTORED] Standardized viewBox & removed hardcoded fill
│   │   ├── ThumbsDown.vue             # [REFACTORED] Standardized SVG component
│   │   └── ThumbsUp.vue               # [REFACTORED] Standardized SVG component
│   ├── AmendmentCarousel.vue          # [UPDATED] Clean icon props & Tailwind classes
│   ├── ProposalContainer.vue          # [UPDATED] Clean icon sizing without wrapper conflicts
│   ├── ProposalDetail.vue             # [UPDATED] Clean icon sizing without wrapper conflicts
│   ├── ProposalExportCard.vue         # [UPDATED] Clean icon props & transitions
│   ├── ProposalVotingCard.vue         # [UPDATED] Removed obsolete :box props; standardized colors
│   ├── Sidebar.vue                    # [UPDATED] Replaced wrapped Filter with clean SVG icon
│   └── VotingControls.vue             # [UPDATED] Clean Tailwind fill & hover state integration
└── views/
    └── HomeView.vue                   # [UPDATED] Clean Stack icon styling

Risks & Mitigations

  • Risk: Visual regression if existing viewBox ratios or path alignments change during normalization.
    • Mitigation: Preserve the exact vector path strings and original coordinate spaces (viewBox="0 0 30 30", 0 0 96 96", etc.) for each respective icon inside its component definition, ensuring 100% pixel-perfect output.
  • Risk: Missing layout spacing when removing wrapper divs (e.g. mx-2, cursor-pointer).
    • Mitigation: Move layout concerns (mx-2, cursor-pointer, flex alignments) directly to the consuming container or parent button elements at all call sites.

Testing

Validation Approach

Verification will follow a multi-tier approach to ensure structural, aesthetic, and functional correctness across the application:

  1. Static Type & Lint Validation: Verify strict type conformity via TypeScript compiler (vue-tsc -b) to ensure no broken imports or invalid prop bindings exist.
  2. Component Integration & Build Validation: Perform full production bundling (vite build) to confirm zero asset resolution issues or tree-shaking failures.
  3. Visual & Interaction Verification: Validate icon rendering across all consuming views, checking sizing, color inheritance, hover states, and responsive behavior.

Key Scenarios

  • Scenario 1: Dynamic Icon Rendering (AppIcon):
    • Render <AppIcon name="confirm" class="text-green-500" /> and <AppIcon name="cancel" class="text-red-500" />.
    • Expected: Correct SVGs render with specified colors, inheriting parent classes properly without extra container nodes.
  • Scenario 2: Direct Component Import & Tailwind Utility Sizing:
    • Render <IconFilter class="size-4 text-white hover:text-slate-200" /> in Sidebar.vue.
    • Expected: Icon scales to 16x16px, displays in white, and transitions to light slate on hover without unwanted wrapper margins.
  • Scenario 3: Interactive Voting Controls & Active States:
    • In VotingControls.vue and ProposalVotingCard.vue, trigger approval and rejection vote states.
    • Expected: Confirm and Cancel icons respond dynamically to positive/negative vote states (fill-green-500, fill-red-500) with smooth transitions.
  • Scenario 4: Carousel Navigation Chevrons:
    • In AmendmentCarousel.vue, navigate through amendments using the left and right arrow buttons.
    • Expected: Arrow icon renders crisply, rotates correctly with Tailwind’s rotate-180 for left navigation, and responds to click events.

Edge Cases

  • Custom Dimension Overrides: Passing explicit numeric size={28} or arbitrary Tailwind strings (class="w-7 h-7") must scale SVG viewports smoothly without clipping.
  • Accessibility Verification: When title is supplied, <svg> must render a <title> node and set role="img"; when omitted, it must set aria-hidden="true".
  • Empty or Missing Styling: Icons with no explicit color class must default to currentColor, seamlessly adopting surrounding text color.

Delivery Steps

✓ Step 1: Establish IconBase and type system

A unified IconBase.vue component and type definitions are created to serve as the standardized foundation for all project icons.

  • Define TypeScript contracts and type definitions in src/components/icons/types.ts (IconName, IconSize, IconBaseProps).
  • Create src/components/icons/IconBase.vue rendering a semantic <svg> root element with default classes (inline-block shrink-0 align-middle select-none), standardized sizing mapping (xs: 12px, sm: 16px, md: 20px, lg: 24px, xl: 32px or custom dimensions), currentColor inheritance, and accessibility attributes (aria-hidden, role="img", title).
  • Create src/components/icons/AppIcon.vue providing dynamic lookup and rendering of icons by name with full type safety and prop forwarding.

✓ Step 2: Refactor and normalize all icon components

All 14 icon components are refactored into clean, single-element SVG components adhering to the standardized base interface.

  • Remove extraneous <div> wrapper containers and hardcoded margins (mx-2) from DownArrow.vue, Filter.vue, Identification.vue, Menu.vue, Plus.vue, and SpeechBubbles.vue.
  • Eliminate hardcoded inline styles (style="fill:#ffffff", style="width: 10px; height: 20px;") and scoped hover overrides (#speech-bubbles:hover, #down-arrow:hover) in favor of standard currentColor / Tailwind class pass-through.
  • Standardize viewBox definitions and geometry across Abstain.vue, Arrow.vue, Cancel.vue, Confirm.vue, Download.vue, Stack.vue, ThumbsDown.vue, ThumbsUp.vue, and other icons.
  • Create src/components/icons/index.ts exporting all icon components as named exports, the dynamic AppIcon component, and relevant TypeScript types for streamlined imports.

✓ Step 3: Migrate consuming components to modernized icons

All existing UI components and views are updated to consume the normalized icon system cleanly with Tailwind styling.

  • Update src/components/Sidebar.vue to use <Filter /> (or <IconFilter />) cleanly inside navigation buttons without unwanted wrapper margins.
  • Update src/components/ProposalContainer.vue and src/components/ProposalDetail.vue to render Identification with proper dimensions and colors.
  • Update src/components/VotingControls.vue and src/components/ProposalVotingCard.vue to remove obsolete :box / :width / :height props from Confirm, Cancel, and Abstain, styling them with Tailwind classes (size-5 text-green-500 hover:text-green-600 fill-current).
  • Update src/components/AmendmentCarousel.vue and src/components/ProposalExportCard.vue to use standard arrow and download icons with seamless transition and hover effects.
  • Update src/views/HomeView.vue to replace inline styles on Stack with standard component props/classes.

✓ Step 4: Verify build integrity, visual fidelity, and accessibility

The modernized icon system is validated across TypeScript compilation, build pipeline, and UI rendering.

  • Execute vue-tsc -b and vite build to guarantee zero type errors or broken import references across the application.
  • Verify visual fidelity, color transitions, hover states, and responsive layout scaling across all pages (Home, Proposal Detail, Voting modal, Carousel).
  • Ensure accessibility tags (aria-hidden="true" on decorative icons and appropriate labels on interactive icon buttons) work correctly without DOM nesting violations.