The frontend component of PuffPastry, meant to interface with the backend, which in turn interfaces with the smart contract.
Não pode escolher mais do que 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.
 
 
 
 
 

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.