提交0.1.0版本

- 完成了书签的基本功能和插件
This commit is contained in:
2026-01-21 23:09:33 +08:00
parent 3e2d1456eb
commit 1a3bbac9ff
95 changed files with 12431 additions and 12445 deletions

View File

@@ -1,19 +1,19 @@
# Server # Server
SERVER_PORT=6667 SERVER_PORT=6667
SERVER_BASE_URL=http://mark.cloud-xl.top:6667 SERVER_BASE_URL=http://mark.cloud-xl.top:6667
# CORS (comma-separated) # CORS (comma-separated)
CORS_ORIGINS=http://mark.cloud-xl.top:6666 CORS_ORIGINS=http://mark.cloud-xl.top:6666
# Postgres (server-side only) # Postgres (server-side only)
DATABASE_HOST=127.0.0.1 DATABASE_HOST=127.0.0.1
DATABASE_PORT=5432 DATABASE_PORT=5432
DATABASE_NAME=postgres DATABASE_NAME=postgres
DATABASE_USER=postgres DATABASE_USER=postgres
DATABASE_PASSWORD=change_me DATABASE_PASSWORD=change_me
DATABASE_SSL=false DATABASE_SSL=false
# Auth # Auth
AUTH_JWT_SECRET=change_me_long_random AUTH_JWT_SECRET=change_me_long_random
# Admin (only this email is treated as admin) # Admin (only this email is treated as admin)
ADMIN_EMAIL=admin@example.com ADMIN_EMAIL=admin@example.com

18
.gitignore vendored
View File

@@ -1,9 +1,9 @@
node_modules/ node_modules/
**/node_modules/ **/node_modules/
dist/ dist/
**/dist/ **/dist/
.env .env
.env.* .env.*
!.env.example !.env.example
.DS_Store .DS_Store
.vscode/ .vscode/

View File

@@ -1,26 +1,26 @@
No,Data Type,Keywords,Best Chart Type,Secondary Options,Color Guidance,Performance Impact,Accessibility Notes,Library Recommendation,Interactive Level No,Data Type,Keywords,Best Chart Type,Secondary Options,Color Guidance,Performance Impact,Accessibility Notes,Library Recommendation,Interactive Level
1,Trend Over Time,"trend, time-series, line, growth, timeline, progress",Line Chart,"Area Chart, Smooth Area",Primary: #0080FF. Multiple series: use distinct colors. Fill: 20% opacity,⚡ Excellent (optimized),✓ Clear line patterns for colorblind users. Add pattern overlays.,"Chart.js, Recharts, ApexCharts",Hover + Zoom 1,Trend Over Time,"trend, time-series, line, growth, timeline, progress",Line Chart,"Area Chart, Smooth Area",Primary: #0080FF. Multiple series: use distinct colors. Fill: 20% opacity,⚡ Excellent (optimized),✓ Clear line patterns for colorblind users. Add pattern overlays.,"Chart.js, Recharts, ApexCharts",Hover + Zoom
2,Compare Categories,"compare, categories, bar, comparison, ranking",Bar Chart (Horizontal or Vertical),"Column Chart, Grouped Bar",Each bar: distinct color. Category: grouped same color. Sorted: descending order,⚡ Excellent,✓ Easy to compare. Add value labels on bars for clarity.,"Chart.js, Recharts, D3.js",Hover + Sort 2,Compare Categories,"compare, categories, bar, comparison, ranking",Bar Chart (Horizontal or Vertical),"Column Chart, Grouped Bar",Each bar: distinct color. Category: grouped same color. Sorted: descending order,⚡ Excellent,✓ Easy to compare. Add value labels on bars for clarity.,"Chart.js, Recharts, D3.js",Hover + Sort
3,Part-to-Whole,"part-to-whole, pie, donut, percentage, proportion, share",Pie Chart or Donut,"Stacked Bar, Treemap",Colors: 5-6 max. Contrasting palette. Large slices first. Use labels.,⚡ Good (limit 6 slices),⚠ Hard for accessibility. Better: Stacked bar with legend. Avoid pie if >5 items.,"Chart.js, Recharts, D3.js",Hover + Drill 3,Part-to-Whole,"part-to-whole, pie, donut, percentage, proportion, share",Pie Chart or Donut,"Stacked Bar, Treemap",Colors: 5-6 max. Contrasting palette. Large slices first. Use labels.,⚡ Good (limit 6 slices),⚠ Hard for accessibility. Better: Stacked bar with legend. Avoid pie if >5 items.,"Chart.js, Recharts, D3.js",Hover + Drill
4,Correlation/Distribution,"correlation, distribution, scatter, relationship, pattern",Scatter Plot or Bubble Chart,"Heat Map, Matrix",Color axis: gradient (blue-red). Size: relative. Opacity: 0.6-0.8 to show density,⚠ Moderate (many points),⚠ Provide data table alternative. Use pattern + color distinction.,"D3.js, Plotly, Recharts",Hover + Brush 4,Correlation/Distribution,"correlation, distribution, scatter, relationship, pattern",Scatter Plot or Bubble Chart,"Heat Map, Matrix",Color axis: gradient (blue-red). Size: relative. Opacity: 0.6-0.8 to show density,⚠ Moderate (many points),⚠ Provide data table alternative. Use pattern + color distinction.,"D3.js, Plotly, Recharts",Hover + Brush
5,Heatmap/Intensity,"heatmap, heat-map, intensity, density, matrix",Heat Map or Choropleth,"Grid Heat Map, Bubble Heat",Gradient: Cool (blue) to Hot (red). Scale: clear legend. Divergent for ±data,⚡ Excellent (color CSS),⚠ Colorblind: Use pattern overlay. Provide numerical legend.,"D3.js, Plotly, ApexCharts",Hover + Zoom 5,Heatmap/Intensity,"heatmap, heat-map, intensity, density, matrix",Heat Map or Choropleth,"Grid Heat Map, Bubble Heat",Gradient: Cool (blue) to Hot (red). Scale: clear legend. Divergent for ±data,⚡ Excellent (color CSS),⚠ Colorblind: Use pattern overlay. Provide numerical legend.,"D3.js, Plotly, ApexCharts",Hover + Zoom
6,Geographic Data,"geographic, map, location, region, geo, spatial","Choropleth Map, Bubble Map",Geographic Heat Map,Regional: single color gradient or categorized colors. Legend: clear scale,⚠ Moderate (rendering),⚠ Include text labels for regions. Provide data table alternative.,"D3.js, Mapbox, Leaflet",Pan + Zoom + Drill 6,Geographic Data,"geographic, map, location, region, geo, spatial","Choropleth Map, Bubble Map",Geographic Heat Map,Regional: single color gradient or categorized colors. Legend: clear scale,⚠ Moderate (rendering),⚠ Include text labels for regions. Provide data table alternative.,"D3.js, Mapbox, Leaflet",Pan + Zoom + Drill
7,Funnel/Flow,funnel/flow,"Funnel Chart, Sankey",Waterfall (for flows),Stages: gradient (starting color → ending color). Show conversion %,⚡ Good,✓ Clear stage labels + percentages. Good for accessibility if labeled.,"D3.js, Recharts, Custom SVG",Hover + Drill 7,Funnel/Flow,funnel/flow,"Funnel Chart, Sankey",Waterfall (for flows),Stages: gradient (starting color → ending color). Show conversion %,⚡ Good,✓ Clear stage labels + percentages. Good for accessibility if labeled.,"D3.js, Recharts, Custom SVG",Hover + Drill
8,Performance vs Target,performance-vs-target,Gauge Chart or Bullet Chart,"Dial, Thermometer",Performance: Red→Yellow→Green gradient. Target: marker line. Threshold colors,⚡ Good,✓ Add numerical value + percentage label beside gauge.,"D3.js, ApexCharts, Custom SVG",Hover 8,Performance vs Target,performance-vs-target,Gauge Chart or Bullet Chart,"Dial, Thermometer",Performance: Red→Yellow→Green gradient. Target: marker line. Threshold colors,⚡ Good,✓ Add numerical value + percentage label beside gauge.,"D3.js, ApexCharts, Custom SVG",Hover
9,Time-Series Forecast,time-series-forecast,Line with Confidence Band,Ribbon Chart,Actual: solid line #0080FF. Forecast: dashed #FF9500. Band: light shading,⚡ Good,✓ Clearly distinguish actual vs forecast. Add legend.,"Chart.js, ApexCharts, Plotly",Hover + Toggle 9,Time-Series Forecast,time-series-forecast,Line with Confidence Band,Ribbon Chart,Actual: solid line #0080FF. Forecast: dashed #FF9500. Band: light shading,⚡ Good,✓ Clearly distinguish actual vs forecast. Add legend.,"Chart.js, ApexCharts, Plotly",Hover + Toggle
10,Anomaly Detection,anomaly-detection,Line Chart with Highlights,Scatter with Alert,Normal: blue #0080FF. Anomaly: red #FF0000 circle/square marker + alert,⚡ Good,✓ Circle/marker for anomalies. Add text alert annotation.,"D3.js, Plotly, ApexCharts",Hover + Alert 10,Anomaly Detection,anomaly-detection,Line Chart with Highlights,Scatter with Alert,Normal: blue #0080FF. Anomaly: red #FF0000 circle/square marker + alert,⚡ Good,✓ Circle/marker for anomalies. Add text alert annotation.,"D3.js, Plotly, ApexCharts",Hover + Alert
11,Hierarchical/Nested Data,hierarchical/nested-data,Treemap,"Sunburst, Nested Donut, Icicle",Parent: distinct hues. Children: lighter shades. White borders 2-3px.,⚠ Moderate,⚠ Poor - provide table alternative. Label large areas.,"D3.js, Recharts, ApexCharts",Hover + Drilldown 11,Hierarchical/Nested Data,hierarchical/nested-data,Treemap,"Sunburst, Nested Donut, Icicle",Parent: distinct hues. Children: lighter shades. White borders 2-3px.,⚠ Moderate,⚠ Poor - provide table alternative. Label large areas.,"D3.js, Recharts, ApexCharts",Hover + Drilldown
12,Flow/Process Data,flow/process-data,Sankey Diagram,"Alluvial, Chord Diagram",Gradient from source to target. Opacity 0.4-0.6 for flows.,⚠ Moderate,⚠ Poor - provide flow table alternative.,"D3.js (d3-sankey), Plotly",Hover + Drilldown 12,Flow/Process Data,flow/process-data,Sankey Diagram,"Alluvial, Chord Diagram",Gradient from source to target. Opacity 0.4-0.6 for flows.,⚠ Moderate,⚠ Poor - provide flow table alternative.,"D3.js (d3-sankey), Plotly",Hover + Drilldown
13,Cumulative Changes,cumulative-changes,Waterfall Chart,"Stacked Bar, Cascade",Increases: #4CAF50. Decreases: #F44336. Start: #2196F3. End: #0D47A1.,⚡ Good,✓ Good - clear directional colors with labels.,"ApexCharts, Highcharts, Plotly",Hover 13,Cumulative Changes,cumulative-changes,Waterfall Chart,"Stacked Bar, Cascade",Increases: #4CAF50. Decreases: #F44336. Start: #2196F3. End: #0D47A1.,⚡ Good,✓ Good - clear directional colors with labels.,"ApexCharts, Highcharts, Plotly",Hover
14,Multi-Variable Comparison,multi-variable-comparison,Radar/Spider Chart,"Parallel Coordinates, Grouped Bar",Single: #0080FF 20% fill. Multiple: distinct colors per dataset.,⚡ Good,⚠ Moderate - limit 5-8 axes. Add data table.,"Chart.js, Recharts, ApexCharts",Hover + Toggle 14,Multi-Variable Comparison,multi-variable-comparison,Radar/Spider Chart,"Parallel Coordinates, Grouped Bar",Single: #0080FF 20% fill. Multiple: distinct colors per dataset.,⚡ Good,⚠ Moderate - limit 5-8 axes. Add data table.,"Chart.js, Recharts, ApexCharts",Hover + Toggle
15,Stock/Trading OHLC,stock/trading-ohlc,Candlestick Chart,"OHLC Bar, Heikin-Ashi",Bullish: #26A69A. Bearish: #EF5350. Volume: 40% opacity below.,⚡ Good,⚠ Moderate - provide OHLC data table.,"Lightweight Charts (TradingView), ApexCharts",Real-time + Hover + Zoom 15,Stock/Trading OHLC,stock/trading-ohlc,Candlestick Chart,"OHLC Bar, Heikin-Ashi",Bullish: #26A69A. Bearish: #EF5350. Volume: 40% opacity below.,⚡ Good,⚠ Moderate - provide OHLC data table.,"Lightweight Charts (TradingView), ApexCharts",Real-time + Hover + Zoom
16,Relationship/Connection Data,relationship/connection-data,Network Graph,"Hierarchical Tree, Adjacency Matrix",Node types: categorical colors. Edges: #90A4AE 60% opacity.,❌ Poor (500+ nodes struggles),❌ Very Poor - provide adjacency list alternative.,"D3.js (d3-force), Vis.js, Cytoscape.js",Drilldown + Hover + Drag 16,Relationship/Connection Data,relationship/connection-data,Network Graph,"Hierarchical Tree, Adjacency Matrix",Node types: categorical colors. Edges: #90A4AE 60% opacity.,❌ Poor (500+ nodes struggles),❌ Very Poor - provide adjacency list alternative.,"D3.js (d3-force), Vis.js, Cytoscape.js",Drilldown + Hover + Drag
17,Distribution/Statistical,distribution/statistical,Box Plot,"Violin Plot, Beeswarm",Box: #BBDEFB. Border: #1976D2. Median: #D32F2F. Outliers: #F44336.,⚡ Excellent,"✓ Good - include stats table (min, Q1, median, Q3, max).","Plotly, D3.js, Chart.js (plugin)",Hover 17,Distribution/Statistical,distribution/statistical,Box Plot,"Violin Plot, Beeswarm",Box: #BBDEFB. Border: #1976D2. Median: #D32F2F. Outliers: #F44336.,⚡ Excellent,"✓ Good - include stats table (min, Q1, median, Q3, max).","Plotly, D3.js, Chart.js (plugin)",Hover
18,Performance vs Target (Compact),performance-vs-target-(compact),Bullet Chart,"Gauge, Progress Bar","Ranges: #FFCDD2, #FFF9C4, #C8E6C9. Performance: #1976D2. Target: black 3px.",⚡ Excellent,✓ Excellent - compact with clear values.,"D3.js, Plotly, Custom SVG",Hover 18,Performance vs Target (Compact),performance-vs-target-(compact),Bullet Chart,"Gauge, Progress Bar","Ranges: #FFCDD2, #FFF9C4, #C8E6C9. Performance: #1976D2. Target: black 3px.",⚡ Excellent,✓ Excellent - compact with clear values.,"D3.js, Plotly, Custom SVG",Hover
19,Proportional/Percentage,proportional/percentage,Waffle Chart,"Pictogram, Stacked Bar 100%",10x10 grid. 3-5 categories max. 2-3px spacing between squares.,⚡ Good,✓ Good - better than pie for accessibility.,"D3.js, React-Waffle, Custom CSS Grid",Hover 19,Proportional/Percentage,proportional/percentage,Waffle Chart,"Pictogram, Stacked Bar 100%",10x10 grid. 3-5 categories max. 2-3px spacing between squares.,⚡ Good,✓ Good - better than pie for accessibility.,"D3.js, React-Waffle, Custom CSS Grid",Hover
20,Hierarchical Proportional,hierarchical-proportional,Sunburst Chart,"Treemap, Icicle, Circle Packing",Center to outer: darker to lighter. 15-20% lighter per level.,⚠ Moderate,⚠ Poor - provide hierarchy table alternative.,"D3.js (d3-hierarchy), Recharts, ApexCharts",Drilldown + Hover 20,Hierarchical Proportional,hierarchical-proportional,Sunburst Chart,"Treemap, Icicle, Circle Packing",Center to outer: darker to lighter. 15-20% lighter per level.,⚠ Moderate,⚠ Poor - provide hierarchy table alternative.,"D3.js (d3-hierarchy), Recharts, ApexCharts",Drilldown + Hover
21,Root Cause Analysis,"root cause, decomposition, tree, hierarchy, drill-down, ai-split",Decomposition Tree,"Decision Tree, Flow Chart",Nodes: #2563EB (Primary) vs #EF4444 (Negative impact). Connectors: Neutral grey.,⚠ Moderate (calculation heavy),✓ clear hierarchy. Allow keyboard navigation for nodes.,"Power BI (native), React-Flow, Custom D3.js",Drill + Expand 21,Root Cause Analysis,"root cause, decomposition, tree, hierarchy, drill-down, ai-split",Decomposition Tree,"Decision Tree, Flow Chart",Nodes: #2563EB (Primary) vs #EF4444 (Negative impact). Connectors: Neutral grey.,⚠ Moderate (calculation heavy),✓ clear hierarchy. Allow keyboard navigation for nodes.,"Power BI (native), React-Flow, Custom D3.js",Drill + Expand
22,3D Spatial Data,"3d, spatial, immersive, terrain, molecular, volumetric",3D Scatter/Surface Plot,"Volumetric Rendering, Point Cloud",Depth cues: lighting/shading. Z-axis: color gradient (cool to warm).,❌ Heavy (WebGL required),❌ Poor - requires alternative 2D view or data table.,"Three.js, Deck.gl, Plotly 3D",Rotate + Zoom + VR 22,3D Spatial Data,"3d, spatial, immersive, terrain, molecular, volumetric",3D Scatter/Surface Plot,"Volumetric Rendering, Point Cloud",Depth cues: lighting/shading. Z-axis: color gradient (cool to warm).,❌ Heavy (WebGL required),❌ Poor - requires alternative 2D view or data table.,"Three.js, Deck.gl, Plotly 3D",Rotate + Zoom + VR
23,Real-Time Streaming,"streaming, real-time, ticker, live, velocity, pulse",Streaming Area Chart,"Ticker Tape, Moving Gauge",Current: Bright Pulse (#00FF00). History: Fading opacity. Grid: Dark.,⚡ Optimized (canvas/webgl),⚠ Flashing elements - provide pause button. High contrast.,Smoothed D3.js, CanvasJS, SciChart,Real-time + Pause 23,Real-Time Streaming,"streaming, real-time, ticker, live, velocity, pulse",Streaming Area Chart,"Ticker Tape, Moving Gauge",Current: Bright Pulse (#00FF00). History: Fading opacity. Grid: Dark.,⚡ Optimized (canvas/webgl),⚠ Flashing elements - provide pause button. High contrast.,Smoothed D3.js, CanvasJS, SciChart,Real-time + Pause
24,Sentiment/Emotion,"sentiment, emotion, nlp, opinion, feeling",Word Cloud with Sentiment,"Sentiment Arc, Radar Chart",Positive: #22C55E. Negative: #EF4444. Neutral: #94A3B8. Size = Frequency.,⚡ Good,⚠ Word clouds poor for screen readers. Use list view.,"D3-cloud, Highcharts, Nivo",Hover + Filter 24,Sentiment/Emotion,"sentiment, emotion, nlp, opinion, feeling",Word Cloud with Sentiment,"Sentiment Arc, Radar Chart",Positive: #22C55E. Negative: #EF4444. Neutral: #94A3B8. Size = Frequency.,⚡ Good,⚠ Word clouds poor for screen readers. Use list view.,"D3-cloud, Highcharts, Nivo",Hover + Filter
25,Process Mining,"process, mining, variants, path, bottleneck, log",Process Map / Graph,"Directed Acyclic Graph (DAG), Petri Net",Happy path: #10B981 (Thick). Deviations: #F59E0B (Thin). Bottlenecks: #EF4444.,⚠ Moderate to Heavy,⚠ Complex graphs hard to navigate. Provide path summary.,"React-Flow, Cytoscape.js, Recharts",Drag + Node-Click 25,Process Mining,"process, mining, variants, path, bottleneck, log",Process Map / Graph,"Directed Acyclic Graph (DAG), Petri Net",Happy path: #10B981 (Thick). Deviations: #F59E0B (Thin). Bottlenecks: #EF4444.,⚠ Moderate to Heavy,⚠ Complex graphs hard to navigate. Provide path summary.,"React-Flow, Cytoscape.js, Recharts",Drag + Node-Click
1 No Data Type Keywords Best Chart Type Secondary Options Color Guidance Performance Impact Accessibility Notes Library Recommendation Interactive Level
2 1 Trend Over Time trend, time-series, line, growth, timeline, progress Line Chart Area Chart, Smooth Area Primary: #0080FF. Multiple series: use distinct colors. Fill: 20% opacity ⚡ Excellent (optimized) ✓ Clear line patterns for colorblind users. Add pattern overlays. Chart.js, Recharts, ApexCharts Hover + Zoom
3 2 Compare Categories compare, categories, bar, comparison, ranking Bar Chart (Horizontal or Vertical) Column Chart, Grouped Bar Each bar: distinct color. Category: grouped same color. Sorted: descending order ⚡ Excellent ✓ Easy to compare. Add value labels on bars for clarity. Chart.js, Recharts, D3.js Hover + Sort
4 3 Part-to-Whole part-to-whole, pie, donut, percentage, proportion, share Pie Chart or Donut Stacked Bar, Treemap Colors: 5-6 max. Contrasting palette. Large slices first. Use labels. ⚡ Good (limit 6 slices) ⚠ Hard for accessibility. Better: Stacked bar with legend. Avoid pie if >5 items. Chart.js, Recharts, D3.js Hover + Drill
5 4 Correlation/Distribution correlation, distribution, scatter, relationship, pattern Scatter Plot or Bubble Chart Heat Map, Matrix Color axis: gradient (blue-red). Size: relative. Opacity: 0.6-0.8 to show density ⚠ Moderate (many points) ⚠ Provide data table alternative. Use pattern + color distinction. D3.js, Plotly, Recharts Hover + Brush
6 5 Heatmap/Intensity heatmap, heat-map, intensity, density, matrix Heat Map or Choropleth Grid Heat Map, Bubble Heat Gradient: Cool (blue) to Hot (red). Scale: clear legend. Divergent for ±data ⚡ Excellent (color CSS) ⚠ Colorblind: Use pattern overlay. Provide numerical legend. D3.js, Plotly, ApexCharts Hover + Zoom
7 6 Geographic Data geographic, map, location, region, geo, spatial Choropleth Map, Bubble Map Geographic Heat Map Regional: single color gradient or categorized colors. Legend: clear scale ⚠ Moderate (rendering) ⚠ Include text labels for regions. Provide data table alternative. D3.js, Mapbox, Leaflet Pan + Zoom + Drill
8 7 Funnel/Flow funnel/flow Funnel Chart, Sankey Waterfall (for flows) Stages: gradient (starting color → ending color). Show conversion % ⚡ Good ✓ Clear stage labels + percentages. Good for accessibility if labeled. D3.js, Recharts, Custom SVG Hover + Drill
9 8 Performance vs Target performance-vs-target Gauge Chart or Bullet Chart Dial, Thermometer Performance: Red→Yellow→Green gradient. Target: marker line. Threshold colors ⚡ Good ✓ Add numerical value + percentage label beside gauge. D3.js, ApexCharts, Custom SVG Hover
10 9 Time-Series Forecast time-series-forecast Line with Confidence Band Ribbon Chart Actual: solid line #0080FF. Forecast: dashed #FF9500. Band: light shading ⚡ Good ✓ Clearly distinguish actual vs forecast. Add legend. Chart.js, ApexCharts, Plotly Hover + Toggle
11 10 Anomaly Detection anomaly-detection Line Chart with Highlights Scatter with Alert Normal: blue #0080FF. Anomaly: red #FF0000 circle/square marker + alert ⚡ Good ✓ Circle/marker for anomalies. Add text alert annotation. D3.js, Plotly, ApexCharts Hover + Alert
12 11 Hierarchical/Nested Data hierarchical/nested-data Treemap Sunburst, Nested Donut, Icicle Parent: distinct hues. Children: lighter shades. White borders 2-3px. ⚠ Moderate ⚠ Poor - provide table alternative. Label large areas. D3.js, Recharts, ApexCharts Hover + Drilldown
13 12 Flow/Process Data flow/process-data Sankey Diagram Alluvial, Chord Diagram Gradient from source to target. Opacity 0.4-0.6 for flows. ⚠ Moderate ⚠ Poor - provide flow table alternative. D3.js (d3-sankey), Plotly Hover + Drilldown
14 13 Cumulative Changes cumulative-changes Waterfall Chart Stacked Bar, Cascade Increases: #4CAF50. Decreases: #F44336. Start: #2196F3. End: #0D47A1. ⚡ Good ✓ Good - clear directional colors with labels. ApexCharts, Highcharts, Plotly Hover
15 14 Multi-Variable Comparison multi-variable-comparison Radar/Spider Chart Parallel Coordinates, Grouped Bar Single: #0080FF 20% fill. Multiple: distinct colors per dataset. ⚡ Good ⚠ Moderate - limit 5-8 axes. Add data table. Chart.js, Recharts, ApexCharts Hover + Toggle
16 15 Stock/Trading OHLC stock/trading-ohlc Candlestick Chart OHLC Bar, Heikin-Ashi Bullish: #26A69A. Bearish: #EF5350. Volume: 40% opacity below. ⚡ Good ⚠ Moderate - provide OHLC data table. Lightweight Charts (TradingView), ApexCharts Real-time + Hover + Zoom
17 16 Relationship/Connection Data relationship/connection-data Network Graph Hierarchical Tree, Adjacency Matrix Node types: categorical colors. Edges: #90A4AE 60% opacity. ❌ Poor (500+ nodes struggles) ❌ Very Poor - provide adjacency list alternative. D3.js (d3-force), Vis.js, Cytoscape.js Drilldown + Hover + Drag
18 17 Distribution/Statistical distribution/statistical Box Plot Violin Plot, Beeswarm Box: #BBDEFB. Border: #1976D2. Median: #D32F2F. Outliers: #F44336. ⚡ Excellent ✓ Good - include stats table (min, Q1, median, Q3, max). Plotly, D3.js, Chart.js (plugin) Hover
19 18 Performance vs Target (Compact) performance-vs-target-(compact) Bullet Chart Gauge, Progress Bar Ranges: #FFCDD2, #FFF9C4, #C8E6C9. Performance: #1976D2. Target: black 3px. ⚡ Excellent ✓ Excellent - compact with clear values. D3.js, Plotly, Custom SVG Hover
20 19 Proportional/Percentage proportional/percentage Waffle Chart Pictogram, Stacked Bar 100% 10x10 grid. 3-5 categories max. 2-3px spacing between squares. ⚡ Good ✓ Good - better than pie for accessibility. D3.js, React-Waffle, Custom CSS Grid Hover
21 20 Hierarchical Proportional hierarchical-proportional Sunburst Chart Treemap, Icicle, Circle Packing Center to outer: darker to lighter. 15-20% lighter per level. ⚠ Moderate ⚠ Poor - provide hierarchy table alternative. D3.js (d3-hierarchy), Recharts, ApexCharts Drilldown + Hover
22 21 Root Cause Analysis root cause, decomposition, tree, hierarchy, drill-down, ai-split Decomposition Tree Decision Tree, Flow Chart Nodes: #2563EB (Primary) vs #EF4444 (Negative impact). Connectors: Neutral grey. ⚠ Moderate (calculation heavy) ✓ clear hierarchy. Allow keyboard navigation for nodes. Power BI (native), React-Flow, Custom D3.js Drill + Expand
23 22 3D Spatial Data 3d, spatial, immersive, terrain, molecular, volumetric 3D Scatter/Surface Plot Volumetric Rendering, Point Cloud Depth cues: lighting/shading. Z-axis: color gradient (cool to warm). ❌ Heavy (WebGL required) ❌ Poor - requires alternative 2D view or data table. Three.js, Deck.gl, Plotly 3D Rotate + Zoom + VR
24 23 Real-Time Streaming streaming, real-time, ticker, live, velocity, pulse Streaming Area Chart Ticker Tape, Moving Gauge Current: Bright Pulse (#00FF00). History: Fading opacity. Grid: Dark. ⚡ Optimized (canvas/webgl) ⚠ Flashing elements - provide pause button. High contrast. Smoothed D3.js CanvasJS
25 24 Sentiment/Emotion sentiment, emotion, nlp, opinion, feeling Word Cloud with Sentiment Sentiment Arc, Radar Chart Positive: #22C55E. Negative: #EF4444. Neutral: #94A3B8. Size = Frequency. ⚡ Good ⚠ Word clouds poor for screen readers. Use list view. D3-cloud, Highcharts, Nivo Hover + Filter
26 25 Process Mining process, mining, variants, path, bottleneck, log Process Map / Graph Directed Acyclic Graph (DAG), Petri Net Happy path: #10B981 (Thick). Deviations: #F59E0B (Thin). Bottlenecks: #EF4444. ⚠ Moderate to Heavy ⚠ Complex graphs hard to navigate. Provide path summary. React-Flow, Cytoscape.js, Recharts Drag + Node-Click

View File

@@ -1,97 +1,97 @@
No,Product Type,Keywords,Primary (Hex),Secondary (Hex),CTA (Hex),Background (Hex),Text (Hex),Border (Hex),Notes No,Product Type,Keywords,Primary (Hex),Secondary (Hex),CTA (Hex),Background (Hex),Text (Hex),Border (Hex),Notes
1,SaaS (General),"saas, general",#2563EB,#3B82F6,#F97316,#F8FAFC,#1E293B,#E2E8F0,Trust blue + accent contrast 1,SaaS (General),"saas, general",#2563EB,#3B82F6,#F97316,#F8FAFC,#1E293B,#E2E8F0,Trust blue + accent contrast
2,Micro SaaS,"micro, saas",#2563EB,#3B82F6,#F97316,#F8FAFC,#1E293B,#E2E8F0,Vibrant primary + white space 2,Micro SaaS,"micro, saas",#2563EB,#3B82F6,#F97316,#F8FAFC,#1E293B,#E2E8F0,Vibrant primary + white space
3,E-commerce,commerce,#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Brand primary + success green 3,E-commerce,commerce,#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Brand primary + success green
4,E-commerce Luxury,"commerce, luxury",#1C1917,#44403C,#CA8A04,#FAFAF9,#0C0A09,#D6D3D1,Premium colors + minimal accent 4,E-commerce Luxury,"commerce, luxury",#1C1917,#44403C,#CA8A04,#FAFAF9,#0C0A09,#D6D3D1,Premium colors + minimal accent
5,Service Landing Page,"service, landing, page",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Brand primary + trust colors 5,Service Landing Page,"service, landing, page",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Brand primary + trust colors
6,B2B Service,"b2b, service",#0F172A,#334155,#0369A1,#F8FAFC,#020617,#E2E8F0,Professional blue + neutral grey 6,B2B Service,"b2b, service",#0F172A,#334155,#0369A1,#F8FAFC,#020617,#E2E8F0,Professional blue + neutral grey
7,Financial Dashboard,"financial, dashboard",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Dark bg + red/green alerts + trust blue 7,Financial Dashboard,"financial, dashboard",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Dark bg + red/green alerts + trust blue
8,Analytics Dashboard,"analytics, dashboard",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Cool→Hot gradients + neutral grey 8,Analytics Dashboard,"analytics, dashboard",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Cool→Hot gradients + neutral grey
9,Healthcare App,"healthcare, app",#0891B2,#22D3EE,#059669,#ECFEFF,#164E63,#A5F3FC,Calm blue + health green + trust 9,Healthcare App,"healthcare, app",#0891B2,#22D3EE,#059669,#ECFEFF,#164E63,#A5F3FC,Calm blue + health green + trust
10,Educational App,"educational, app",#4F46E5,#818CF8,#F97316,#EEF2FF,#1E1B4B,#C7D2FE,Playful colors + clear hierarchy 10,Educational App,"educational, app",#4F46E5,#818CF8,#F97316,#EEF2FF,#1E1B4B,#C7D2FE,Playful colors + clear hierarchy
11,Creative Agency,"creative, agency",#EC4899,#F472B6,#06B6D4,#FDF2F8,#831843,#FBCFE8,Bold primaries + artistic freedom 11,Creative Agency,"creative, agency",#EC4899,#F472B6,#06B6D4,#FDF2F8,#831843,#FBCFE8,Bold primaries + artistic freedom
12,Portfolio/Personal,"portfolio, personal",#18181B,#3F3F46,#2563EB,#FAFAFA,#09090B,#E4E4E7,Brand primary + artistic interpretation 12,Portfolio/Personal,"portfolio, personal",#18181B,#3F3F46,#2563EB,#FAFAFA,#09090B,#E4E4E7,Brand primary + artistic interpretation
13,Gaming,gaming,#7C3AED,#A78BFA,#F43F5E,#0F0F23,#E2E8F0,#4C1D95,Vibrant + neon + immersive colors 13,Gaming,gaming,#7C3AED,#A78BFA,#F43F5E,#0F0F23,#E2E8F0,#4C1D95,Vibrant + neon + immersive colors
14,Government/Public Service,"government, public, service",#0F172A,#334155,#0369A1,#F8FAFC,#020617,#E2E8F0,Professional blue + high contrast 14,Government/Public Service,"government, public, service",#0F172A,#334155,#0369A1,#F8FAFC,#020617,#E2E8F0,Professional blue + high contrast
15,Fintech/Crypto,"fintech, crypto",#F59E0B,#FBBF24,#8B5CF6,#0F172A,#F8FAFC,#334155,Dark tech colors + trust + vibrant accents 15,Fintech/Crypto,"fintech, crypto",#F59E0B,#FBBF24,#8B5CF6,#0F172A,#F8FAFC,#334155,Dark tech colors + trust + vibrant accents
16,Social Media App,"social, media, app",#2563EB,#60A5FA,#F43F5E,#F8FAFC,#1E293B,#DBEAFE,Vibrant + engagement colors 16,Social Media App,"social, media, app",#2563EB,#60A5FA,#F43F5E,#F8FAFC,#1E293B,#DBEAFE,Vibrant + engagement colors
17,Productivity Tool,"productivity, tool",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Clear hierarchy + functional colors 17,Productivity Tool,"productivity, tool",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Clear hierarchy + functional colors
18,Design System/Component Library,"design, system, component, library",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Clear hierarchy + code-like structure 18,Design System/Component Library,"design, system, component, library",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Clear hierarchy + code-like structure
19,AI/Chatbot Platform,"chatbot, platform",#7C3AED,#A78BFA,#06B6D4,#FAF5FF,#1E1B4B,#DDD6FE,Neutral + AI Purple (#6366F1) 19,AI/Chatbot Platform,"chatbot, platform",#7C3AED,#A78BFA,#06B6D4,#FAF5FF,#1E1B4B,#DDD6FE,Neutral + AI Purple (#6366F1)
20,NFT/Web3 Platform,"nft, web3, platform",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Dark + Neon + Gold (#FFD700) 20,NFT/Web3 Platform,"nft, web3, platform",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Dark + Neon + Gold (#FFD700)
21,Creator Economy Platform,"creator, economy, platform",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Vibrant + Brand colors 21,Creator Economy Platform,"creator, economy, platform",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Vibrant + Brand colors
22,Sustainability/ESG Platform,"sustainability, esg, platform",#7C3AED,#A78BFA,#06B6D4,#FAF5FF,#1E1B4B,#DDD6FE,Green (#228B22) + Earth tones 22,Sustainability/ESG Platform,"sustainability, esg, platform",#7C3AED,#A78BFA,#06B6D4,#FAF5FF,#1E1B4B,#DDD6FE,Green (#228B22) + Earth tones
23,Remote Work/Collaboration Tool,"remote, work, collaboration, tool",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Calm Blue + Neutral grey 23,Remote Work/Collaboration Tool,"remote, work, collaboration, tool",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Calm Blue + Neutral grey
24,Mental Health App,"mental, health, app",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Calm Pastels + Trust colors 24,Mental Health App,"mental, health, app",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Calm Pastels + Trust colors
25,Pet Tech App,"pet, tech, app",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Playful + Warm colors 25,Pet Tech App,"pet, tech, app",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Playful + Warm colors
26,Smart Home/IoT Dashboard,"smart, home, iot, dashboard",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Dark + Status indicator colors 26,Smart Home/IoT Dashboard,"smart, home, iot, dashboard",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Dark + Status indicator colors
27,EV/Charging Ecosystem,"charging, ecosystem",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Electric Blue (#009CD1) + Green 27,EV/Charging Ecosystem,"charging, ecosystem",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Electric Blue (#009CD1) + Green
28,Subscription Box Service,"subscription, box, service",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Brand + Excitement colors 28,Subscription Box Service,"subscription, box, service",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Brand + Excitement colors
29,Podcast Platform,"podcast, platform",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Dark + Audio waveform accents 29,Podcast Platform,"podcast, platform",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Dark + Audio waveform accents
30,Dating App,"dating, app",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Warm + Romantic (Pink/Red gradients) 30,Dating App,"dating, app",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Warm + Romantic (Pink/Red gradients)
31,Micro-Credentials/Badges Platform,"micro, credentials, badges, platform",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Trust Blue + Gold (#FFD700) 31,Micro-Credentials/Badges Platform,"micro, credentials, badges, platform",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Trust Blue + Gold (#FFD700)
32,Knowledge Base/Documentation,"knowledge, base, documentation",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Clean hierarchy + minimal color 32,Knowledge Base/Documentation,"knowledge, base, documentation",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Clean hierarchy + minimal color
33,Hyperlocal Services,"hyperlocal, services",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Location markers + Trust colors 33,Hyperlocal Services,"hyperlocal, services",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Location markers + Trust colors
34,Beauty/Spa/Wellness Service,"beauty, spa, wellness, service",#10B981,#34D399,#8B5CF6,#ECFDF5,#064E3B,#A7F3D0,Soft pastels (Pink #FFB6C1 Sage #90EE90) + Cream + Gold accents 34,Beauty/Spa/Wellness Service,"beauty, spa, wellness, service",#10B981,#34D399,#8B5CF6,#ECFDF5,#064E3B,#A7F3D0,Soft pastels (Pink #FFB6C1 Sage #90EE90) + Cream + Gold accents
35,Luxury/Premium Brand,"luxury, premium, brand",#1C1917,#44403C,#CA8A04,#FAFAF9,#0C0A09,#D6D3D1,Black + Gold (#FFD700) + White + Minimal accent 35,Luxury/Premium Brand,"luxury, premium, brand",#1C1917,#44403C,#CA8A04,#FAFAF9,#0C0A09,#D6D3D1,Black + Gold (#FFD700) + White + Minimal accent
36,Restaurant/Food Service,"restaurant, food, service",#DC2626,#F87171,#CA8A04,#FEF2F2,#450A0A,#FECACA,Warm colors (Orange Red Brown) + appetizing imagery 36,Restaurant/Food Service,"restaurant, food, service",#DC2626,#F87171,#CA8A04,#FEF2F2,#450A0A,#FECACA,Warm colors (Orange Red Brown) + appetizing imagery
37,Fitness/Gym App,"fitness, gym, app",#DC2626,#F87171,#16A34A,#FEF2F2,#1F2937,#FECACA,Energetic (Orange #FF6B35 Electric Blue) + Dark bg 37,Fitness/Gym App,"fitness, gym, app",#DC2626,#F87171,#16A34A,#FEF2F2,#1F2937,#FECACA,Energetic (Orange #FF6B35 Electric Blue) + Dark bg
38,Real Estate/Property,"real, estate, property",#0F766E,#14B8A6,#0369A1,#F0FDFA,#134E4A,#99F6E4,Trust Blue (#0077B6) + Gold accents + White 38,Real Estate/Property,"real, estate, property",#0F766E,#14B8A6,#0369A1,#F0FDFA,#134E4A,#99F6E4,Trust Blue (#0077B6) + Gold accents + White
39,Travel/Tourism Agency,"travel, tourism, agency",#EC4899,#F472B6,#06B6D4,#FDF2F8,#831843,#FBCFE8,Vibrant destination colors + Sky Blue + Warm accents 39,Travel/Tourism Agency,"travel, tourism, agency",#EC4899,#F472B6,#06B6D4,#FDF2F8,#831843,#FBCFE8,Vibrant destination colors + Sky Blue + Warm accents
40,Hotel/Hospitality,"hotel, hospitality",#1E3A8A,#3B82F6,#CA8A04,#F8FAFC,#1E40AF,#BFDBFE,Warm neutrals + Gold (#D4AF37) + Brand accent 40,Hotel/Hospitality,"hotel, hospitality",#1E3A8A,#3B82F6,#CA8A04,#F8FAFC,#1E40AF,#BFDBFE,Warm neutrals + Gold (#D4AF37) + Brand accent
41,Wedding/Event Planning,"wedding, event, planning",#7C3AED,#A78BFA,#F97316,#FAF5FF,#4C1D95,#DDD6FE,Soft Pink (#FFD6E0) + Gold + Cream + Sage 41,Wedding/Event Planning,"wedding, event, planning",#7C3AED,#A78BFA,#F97316,#FAF5FF,#4C1D95,#DDD6FE,Soft Pink (#FFD6E0) + Gold + Cream + Sage
42,Legal Services,"legal, services",#1E3A8A,#1E40AF,#B45309,#F8FAFC,#0F172A,#CBD5E1,Navy Blue (#1E3A5F) + Gold + White 42,Legal Services,"legal, services",#1E3A8A,#1E40AF,#B45309,#F8FAFC,#0F172A,#CBD5E1,Navy Blue (#1E3A5F) + Gold + White
43,Insurance Platform,"insurance, platform",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Trust Blue (#0066CC) + Green (security) + Neutral 43,Insurance Platform,"insurance, platform",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Trust Blue (#0066CC) + Green (security) + Neutral
44,Banking/Traditional Finance,"banking, traditional, finance",#0F766E,#14B8A6,#0369A1,#F0FDFA,#134E4A,#99F6E4,Navy (#0A1628) + Trust Blue + Gold accents 44,Banking/Traditional Finance,"banking, traditional, finance",#0F766E,#14B8A6,#0369A1,#F0FDFA,#134E4A,#99F6E4,Navy (#0A1628) + Trust Blue + Gold accents
45,Online Course/E-learning,"online, course, learning",#0D9488,#2DD4BF,#EA580C,#F0FDFA,#134E4A,#5EEAD4,Vibrant learning colors + Progress green 45,Online Course/E-learning,"online, course, learning",#0D9488,#2DD4BF,#EA580C,#F0FDFA,#134E4A,#5EEAD4,Vibrant learning colors + Progress green
46,Non-profit/Charity,"non, profit, charity",#0891B2,#22D3EE,#F97316,#ECFEFF,#164E63,#A5F3FC,Cause-related colors + Trust + Warm 46,Non-profit/Charity,"non, profit, charity",#0891B2,#22D3EE,#F97316,#ECFEFF,#164E63,#A5F3FC,Cause-related colors + Trust + Warm
47,Music Streaming,"music, streaming",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Dark (#121212) + Vibrant accents + Album art colors 47,Music Streaming,"music, streaming",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Dark (#121212) + Vibrant accents + Album art colors
48,Video Streaming/OTT,"video, streaming, ott",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Dark bg + Content poster colors + Brand accent 48,Video Streaming/OTT,"video, streaming, ott",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Dark bg + Content poster colors + Brand accent
49,Job Board/Recruitment,"job, board, recruitment",#0F172A,#334155,#0369A1,#F8FAFC,#020617,#E2E8F0,Professional Blue + Success Green + Neutral 49,Job Board/Recruitment,"job, board, recruitment",#0F172A,#334155,#0369A1,#F8FAFC,#020617,#E2E8F0,Professional Blue + Success Green + Neutral
50,Marketplace (P2P),"marketplace, p2p",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Trust colors + Category colors + Success green 50,Marketplace (P2P),"marketplace, p2p",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Trust colors + Category colors + Success green
51,Logistics/Delivery,"logistics, delivery",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Blue (#2563EB) + Orange (tracking) + Green (delivered) 51,Logistics/Delivery,"logistics, delivery",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Blue (#2563EB) + Orange (tracking) + Green (delivered)
52,Agriculture/Farm Tech,"agriculture, farm, tech",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Earth Green (#4A7C23) + Brown + Sky Blue 52,Agriculture/Farm Tech,"agriculture, farm, tech",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Earth Green (#4A7C23) + Brown + Sky Blue
53,Construction/Architecture,"construction, architecture",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Grey (#4A4A4A) + Orange (safety) + Blueprint Blue 53,Construction/Architecture,"construction, architecture",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Grey (#4A4A4A) + Orange (safety) + Blueprint Blue
54,Automotive/Car Dealership,"automotive, car, dealership",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Brand colors + Metallic accents + Dark/Light 54,Automotive/Car Dealership,"automotive, car, dealership",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Brand colors + Metallic accents + Dark/Light
55,Photography Studio,"photography, studio",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Black + White + Minimal accent 55,Photography Studio,"photography, studio",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Black + White + Minimal accent
56,Coworking Space,"coworking, space",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Energetic colors + Wood tones + Brand accent 56,Coworking Space,"coworking, space",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Energetic colors + Wood tones + Brand accent
57,Cleaning Service,"cleaning, service",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Fresh Blue (#00B4D8) + Clean White + Green 57,Cleaning Service,"cleaning, service",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Fresh Blue (#00B4D8) + Clean White + Green
58,Home Services (Plumber/Electrician),"home, services, plumber, electrician",#0F172A,#334155,#0369A1,#F8FAFC,#020617,#E2E8F0,Trust Blue + Safety Orange + Professional grey 58,Home Services (Plumber/Electrician),"home, services, plumber, electrician",#0F172A,#334155,#0369A1,#F8FAFC,#020617,#E2E8F0,Trust Blue + Safety Orange + Professional grey
59,Childcare/Daycare,"childcare, daycare",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Playful pastels + Safe colors + Warm accents 59,Childcare/Daycare,"childcare, daycare",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Playful pastels + Safe colors + Warm accents
60,Senior Care/Elderly,"senior, care, elderly",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Calm Blue + Warm neutrals + Large text 60,Senior Care/Elderly,"senior, care, elderly",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Calm Blue + Warm neutrals + Large text
61,Medical Clinic,"medical, clinic",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Medical Blue (#0077B6) + Trust White + Calm Green 61,Medical Clinic,"medical, clinic",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Medical Blue (#0077B6) + Trust White + Calm Green
62,Pharmacy/Drug Store,"pharmacy, drug, store",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Pharmacy Green + Trust Blue + Clean White 62,Pharmacy/Drug Store,"pharmacy, drug, store",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Pharmacy Green + Trust Blue + Clean White
63,Dental Practice,"dental, practice",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Fresh Blue + White + Smile Yellow accent 63,Dental Practice,"dental, practice",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Fresh Blue + White + Smile Yellow accent
64,Veterinary Clinic,"veterinary, clinic",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Caring Blue + Pet-friendly colors + Warm accents 64,Veterinary Clinic,"veterinary, clinic",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Caring Blue + Pet-friendly colors + Warm accents
65,Florist/Plant Shop,"florist, plant, shop",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Natural Green + Floral pinks/purples + Earth tones 65,Florist/Plant Shop,"florist, plant, shop",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Natural Green + Floral pinks/purples + Earth tones
66,Bakery/Cafe,"bakery, cafe",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Warm Brown + Cream + Appetizing accents 66,Bakery/Cafe,"bakery, cafe",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Warm Brown + Cream + Appetizing accents
67,Coffee Shop,"coffee, shop",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Coffee Brown (#6F4E37) + Cream + Warm accents 67,Coffee Shop,"coffee, shop",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Coffee Brown (#6F4E37) + Cream + Warm accents
68,Brewery/Winery,"brewery, winery",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Deep amber/burgundy + Gold + Craft aesthetic 68,Brewery/Winery,"brewery, winery",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Deep amber/burgundy + Gold + Craft aesthetic
69,Airline,airline,#7C3AED,#A78BFA,#06B6D4,#FAF5FF,#1E1B4B,#DDD6FE,Sky Blue + Brand colors + Trust accents 69,Airline,airline,#7C3AED,#A78BFA,#06B6D4,#FAF5FF,#1E1B4B,#DDD6FE,Sky Blue + Brand colors + Trust accents
70,News/Media Platform,"news, media, platform",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Brand colors + High contrast + Category colors 70,News/Media Platform,"news, media, platform",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Brand colors + High contrast + Category colors
71,Magazine/Blog,"magazine, blog",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Editorial colors + Brand primary + Clean white 71,Magazine/Blog,"magazine, blog",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Editorial colors + Brand primary + Clean white
72,Freelancer Platform,"freelancer, platform",#0F172A,#334155,#0369A1,#F8FAFC,#020617,#E2E8F0,Professional Blue + Success Green + Neutral 72,Freelancer Platform,"freelancer, platform",#0F172A,#334155,#0369A1,#F8FAFC,#020617,#E2E8F0,Professional Blue + Success Green + Neutral
73,Consulting Firm,"consulting, firm",#0F172A,#334155,#0369A1,#F8FAFC,#020617,#E2E8F0,Navy + Gold + Professional grey 73,Consulting Firm,"consulting, firm",#0F172A,#334155,#0369A1,#F8FAFC,#020617,#E2E8F0,Navy + Gold + Professional grey
74,Marketing Agency,"marketing, agency",#EC4899,#F472B6,#06B6D4,#FDF2F8,#831843,#FBCFE8,Bold brand colors + Creative freedom 74,Marketing Agency,"marketing, agency",#EC4899,#F472B6,#06B6D4,#FDF2F8,#831843,#FBCFE8,Bold brand colors + Creative freedom
75,Event Management,"event, management",#7C3AED,#A78BFA,#F97316,#FAF5FF,#4C1D95,#DDD6FE,Event theme colors + Excitement accents 75,Event Management,"event, management",#7C3AED,#A78BFA,#F97316,#FAF5FF,#4C1D95,#DDD6FE,Event theme colors + Excitement accents
76,Conference/Webinar Platform,"conference, webinar, platform",#0F172A,#334155,#0369A1,#F8FAFC,#020617,#E2E8F0,Professional Blue + Video accent + Brand 76,Conference/Webinar Platform,"conference, webinar, platform",#0F172A,#334155,#0369A1,#F8FAFC,#020617,#E2E8F0,Professional Blue + Video accent + Brand
77,Membership/Community,"membership, community",#7C3AED,#A78BFA,#F97316,#FAF5FF,#4C1D95,#DDD6FE,Community brand colors + Engagement accents 77,Membership/Community,"membership, community",#7C3AED,#A78BFA,#F97316,#FAF5FF,#4C1D95,#DDD6FE,Community brand colors + Engagement accents
78,Newsletter Platform,"newsletter, platform",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Brand primary + Clean white + CTA accent 78,Newsletter Platform,"newsletter, platform",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Brand primary + Clean white + CTA accent
79,Digital Products/Downloads,"digital, products, downloads",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Product category colors + Brand + Success green 79,Digital Products/Downloads,"digital, products, downloads",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Product category colors + Brand + Success green
80,Church/Religious Organization,"church, religious, organization",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Warm Gold + Deep Purple/Blue + White 80,Church/Religious Organization,"church, religious, organization",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Warm Gold + Deep Purple/Blue + White
81,Sports Team/Club,"sports, team, club",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Team colors + Energetic accents 81,Sports Team/Club,"sports, team, club",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Team colors + Energetic accents
82,Museum/Gallery,"museum, gallery",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Art-appropriate neutrals + Exhibition accents 82,Museum/Gallery,"museum, gallery",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Art-appropriate neutrals + Exhibition accents
83,Theater/Cinema,"theater, cinema",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Dark + Spotlight accents + Gold 83,Theater/Cinema,"theater, cinema",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Dark + Spotlight accents + Gold
84,Language Learning App,"language, learning, app",#0D9488,#2DD4BF,#EA580C,#F0FDFA,#134E4A,#5EEAD4,Playful colors + Progress indicators + Country flags 84,Language Learning App,"language, learning, app",#0D9488,#2DD4BF,#EA580C,#F0FDFA,#134E4A,#5EEAD4,Playful colors + Progress indicators + Country flags
85,Coding Bootcamp,"coding, bootcamp",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Code editor colors + Brand + Success green 85,Coding Bootcamp,"coding, bootcamp",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Code editor colors + Brand + Success green
86,Cybersecurity Platform,"cybersecurity, security, cyber, hacker",#00FF41,#0D0D0D,#00FF41,#000000,#E0E0E0,#1F1F1F,Matrix Green + Deep Black + Terminal feel 86,Cybersecurity Platform,"cybersecurity, security, cyber, hacker",#00FF41,#0D0D0D,#00FF41,#000000,#E0E0E0,#1F1F1F,Matrix Green + Deep Black + Terminal feel
87,Developer Tool / IDE,"developer, tool, ide, code, dev",#3B82F6,#1E293B,#2563EB,#0F172A,#F1F5F9,#334155,Dark syntax theme colors + Blue focus 87,Developer Tool / IDE,"developer, tool, ide, code, dev",#3B82F6,#1E293B,#2563EB,#0F172A,#F1F5F9,#334155,Dark syntax theme colors + Blue focus
88,Biotech / Life Sciences,"biotech, science, biology, medical",#0EA5E9,#0284C7,#10B981,#F8FAFC,#0F172A,#E2E8F0,Sterile White + DNA Blue + Life Green 88,Biotech / Life Sciences,"biotech, science, biology, medical",#0EA5E9,#0284C7,#10B981,#F8FAFC,#0F172A,#E2E8F0,Sterile White + DNA Blue + Life Green
89,Space Tech / Aerospace,"space, aerospace, tech, futuristic",#FFFFFF,#94A3B8,#3B82F6,#0B0B10,#F8FAFC,#1E293B,Deep Space Black + Star White + Metallic 89,Space Tech / Aerospace,"space, aerospace, tech, futuristic",#FFFFFF,#94A3B8,#3B82F6,#0B0B10,#F8FAFC,#1E293B,Deep Space Black + Star White + Metallic
90,Architecture / Interior,"architecture, interior, design, luxury",#171717,#404040,#D4AF37,#FFFFFF,#171717,#E5E5E5,Monochrome + Gold Accent + High Imagery 90,Architecture / Interior,"architecture, interior, design, luxury",#171717,#404040,#D4AF37,#FFFFFF,#171717,#E5E5E5,Monochrome + Gold Accent + High Imagery
91,Quantum Computing,"quantum, qubit, tech",#00FFFF,#7B61FF,#FF00FF,#050510,#E0E0FF,#333344,Interference patterns + Neon + Deep Dark 91,Quantum Computing,"quantum, qubit, tech",#00FFFF,#7B61FF,#FF00FF,#050510,#E0E0FF,#333344,Interference patterns + Neon + Deep Dark
92,Biohacking / Longevity,"bio, health, science",#FF4D4D,#4D94FF,#00E676,#F5F5F7,#1C1C1E,#E5E5EA,Biological red/blue + Clinical white 92,Biohacking / Longevity,"bio, health, science",#FF4D4D,#4D94FF,#00E676,#F5F5F7,#1C1C1E,#E5E5EA,Biological red/blue + Clinical white
93,Autonomous Systems,"drone, robot, fleet",#00FF41,#008F11,#FF3333,#0D1117,#E6EDF3,#30363D,Terminal Green + Tactical Dark 93,Autonomous Systems,"drone, robot, fleet",#00FF41,#008F11,#FF3333,#0D1117,#E6EDF3,#30363D,Terminal Green + Tactical Dark
94,Generative AI Art,"art, gen-ai, creative",#111111,#333333,#FFFFFF,#FAFAFA,#000000,#E5E5E5,Canvas Neutral + High Contrast 94,Generative AI Art,"art, gen-ai, creative",#111111,#333333,#FFFFFF,#FAFAFA,#000000,#E5E5E5,Canvas Neutral + High Contrast
95,Spatial / Vision OS,"spatial, glass, vision",#FFFFFF,#E5E5E5,#007AFF,#888888,#000000,#FFFFFF,Glass opacity 20% + System Blue 95,Spatial / Vision OS,"spatial, glass, vision",#FFFFFF,#E5E5E5,#007AFF,#888888,#000000,#FFFFFF,Glass opacity 20% + System Blue
96,Climate Tech,"climate, green, energy",#2E8B57,#87CEEB,#FFD700,#F0FFF4,#1A3320,#C6E6C6,Nature Green + Solar Yellow + Air Blue 96,Climate Tech,"climate, green, energy",#2E8B57,#87CEEB,#FFD700,#F0FFF4,#1A3320,#C6E6C6,Nature Green + Solar Yellow + Air Blue
1 No Product Type Keywords Primary (Hex) Secondary (Hex) CTA (Hex) Background (Hex) Text (Hex) Border (Hex) Notes
2 1 SaaS (General) saas, general #2563EB #3B82F6 #F97316 #F8FAFC #1E293B #E2E8F0 Trust blue + accent contrast
3 2 Micro SaaS micro, saas #2563EB #3B82F6 #F97316 #F8FAFC #1E293B #E2E8F0 Vibrant primary + white space
4 3 E-commerce commerce #3B82F6 #60A5FA #F97316 #F8FAFC #1E293B #E2E8F0 Brand primary + success green
5 4 E-commerce Luxury commerce, luxury #1C1917 #44403C #CA8A04 #FAFAF9 #0C0A09 #D6D3D1 Premium colors + minimal accent
6 5 Service Landing Page service, landing, page #3B82F6 #60A5FA #F97316 #F8FAFC #1E293B #E2E8F0 Brand primary + trust colors
7 6 B2B Service b2b, service #0F172A #334155 #0369A1 #F8FAFC #020617 #E2E8F0 Professional blue + neutral grey
8 7 Financial Dashboard financial, dashboard #3B82F6 #60A5FA #F97316 #F8FAFC #1E293B #E2E8F0 Dark bg + red/green alerts + trust blue
9 8 Analytics Dashboard analytics, dashboard #3B82F6 #60A5FA #F97316 #F8FAFC #1E293B #E2E8F0 Cool→Hot gradients + neutral grey
10 9 Healthcare App healthcare, app #0891B2 #22D3EE #059669 #ECFEFF #164E63 #A5F3FC Calm blue + health green + trust
11 10 Educational App educational, app #4F46E5 #818CF8 #F97316 #EEF2FF #1E1B4B #C7D2FE Playful colors + clear hierarchy
12 11 Creative Agency creative, agency #EC4899 #F472B6 #06B6D4 #FDF2F8 #831843 #FBCFE8 Bold primaries + artistic freedom
13 12 Portfolio/Personal portfolio, personal #18181B #3F3F46 #2563EB #FAFAFA #09090B #E4E4E7 Brand primary + artistic interpretation
14 13 Gaming gaming #7C3AED #A78BFA #F43F5E #0F0F23 #E2E8F0 #4C1D95 Vibrant + neon + immersive colors
15 14 Government/Public Service government, public, service #0F172A #334155 #0369A1 #F8FAFC #020617 #E2E8F0 Professional blue + high contrast
16 15 Fintech/Crypto fintech, crypto #F59E0B #FBBF24 #8B5CF6 #0F172A #F8FAFC #334155 Dark tech colors + trust + vibrant accents
17 16 Social Media App social, media, app #2563EB #60A5FA #F43F5E #F8FAFC #1E293B #DBEAFE Vibrant + engagement colors
18 17 Productivity Tool productivity, tool #3B82F6 #60A5FA #F97316 #F8FAFC #1E293B #E2E8F0 Clear hierarchy + functional colors
19 18 Design System/Component Library design, system, component, library #3B82F6 #60A5FA #F97316 #F8FAFC #1E293B #E2E8F0 Clear hierarchy + code-like structure
20 19 AI/Chatbot Platform chatbot, platform #7C3AED #A78BFA #06B6D4 #FAF5FF #1E1B4B #DDD6FE Neutral + AI Purple (#6366F1)
21 20 NFT/Web3 Platform nft, web3, platform #3B82F6 #60A5FA #F97316 #F8FAFC #1E293B #E2E8F0 Dark + Neon + Gold (#FFD700)
22 21 Creator Economy Platform creator, economy, platform #3B82F6 #60A5FA #F97316 #F8FAFC #1E293B #E2E8F0 Vibrant + Brand colors
23 22 Sustainability/ESG Platform sustainability, esg, platform #7C3AED #A78BFA #06B6D4 #FAF5FF #1E1B4B #DDD6FE Green (#228B22) + Earth tones
24 23 Remote Work/Collaboration Tool remote, work, collaboration, tool #3B82F6 #60A5FA #F97316 #F8FAFC #1E293B #E2E8F0 Calm Blue + Neutral grey
25 24 Mental Health App mental, health, app #3B82F6 #60A5FA #F97316 #F8FAFC #1E293B #E2E8F0 Calm Pastels + Trust colors
26 25 Pet Tech App pet, tech, app #3B82F6 #60A5FA #F97316 #F8FAFC #1E293B #E2E8F0 Playful + Warm colors
27 26 Smart Home/IoT Dashboard smart, home, iot, dashboard #3B82F6 #60A5FA #F97316 #F8FAFC #1E293B #E2E8F0 Dark + Status indicator colors
28 27 EV/Charging Ecosystem charging, ecosystem #3B82F6 #60A5FA #F97316 #F8FAFC #1E293B #E2E8F0 Electric Blue (#009CD1) + Green
29 28 Subscription Box Service subscription, box, service #3B82F6 #60A5FA #F97316 #F8FAFC #1E293B #E2E8F0 Brand + Excitement colors
30 29 Podcast Platform podcast, platform #3B82F6 #60A5FA #F97316 #F8FAFC #1E293B #E2E8F0 Dark + Audio waveform accents
31 30 Dating App dating, app #3B82F6 #60A5FA #F97316 #F8FAFC #1E293B #E2E8F0 Warm + Romantic (Pink/Red gradients)
32 31 Micro-Credentials/Badges Platform micro, credentials, badges, platform #3B82F6 #60A5FA #F97316 #F8FAFC #1E293B #E2E8F0 Trust Blue + Gold (#FFD700)
33 32 Knowledge Base/Documentation knowledge, base, documentation #3B82F6 #60A5FA #F97316 #F8FAFC #1E293B #E2E8F0 Clean hierarchy + minimal color
34 33 Hyperlocal Services hyperlocal, services #3B82F6 #60A5FA #F97316 #F8FAFC #1E293B #E2E8F0 Location markers + Trust colors
35 34 Beauty/Spa/Wellness Service beauty, spa, wellness, service #10B981 #34D399 #8B5CF6 #ECFDF5 #064E3B #A7F3D0 Soft pastels (Pink #FFB6C1 Sage #90EE90) + Cream + Gold accents
36 35 Luxury/Premium Brand luxury, premium, brand #1C1917 #44403C #CA8A04 #FAFAF9 #0C0A09 #D6D3D1 Black + Gold (#FFD700) + White + Minimal accent
37 36 Restaurant/Food Service restaurant, food, service #DC2626 #F87171 #CA8A04 #FEF2F2 #450A0A #FECACA Warm colors (Orange Red Brown) + appetizing imagery
38 37 Fitness/Gym App fitness, gym, app #DC2626 #F87171 #16A34A #FEF2F2 #1F2937 #FECACA Energetic (Orange #FF6B35 Electric Blue) + Dark bg
39 38 Real Estate/Property real, estate, property #0F766E #14B8A6 #0369A1 #F0FDFA #134E4A #99F6E4 Trust Blue (#0077B6) + Gold accents + White
40 39 Travel/Tourism Agency travel, tourism, agency #EC4899 #F472B6 #06B6D4 #FDF2F8 #831843 #FBCFE8 Vibrant destination colors + Sky Blue + Warm accents
41 40 Hotel/Hospitality hotel, hospitality #1E3A8A #3B82F6 #CA8A04 #F8FAFC #1E40AF #BFDBFE Warm neutrals + Gold (#D4AF37) + Brand accent
42 41 Wedding/Event Planning wedding, event, planning #7C3AED #A78BFA #F97316 #FAF5FF #4C1D95 #DDD6FE Soft Pink (#FFD6E0) + Gold + Cream + Sage
43 42 Legal Services legal, services #1E3A8A #1E40AF #B45309 #F8FAFC #0F172A #CBD5E1 Navy Blue (#1E3A5F) + Gold + White
44 43 Insurance Platform insurance, platform #3B82F6 #60A5FA #F97316 #F8FAFC #1E293B #E2E8F0 Trust Blue (#0066CC) + Green (security) + Neutral
45 44 Banking/Traditional Finance banking, traditional, finance #0F766E #14B8A6 #0369A1 #F0FDFA #134E4A #99F6E4 Navy (#0A1628) + Trust Blue + Gold accents
46 45 Online Course/E-learning online, course, learning #0D9488 #2DD4BF #EA580C #F0FDFA #134E4A #5EEAD4 Vibrant learning colors + Progress green
47 46 Non-profit/Charity non, profit, charity #0891B2 #22D3EE #F97316 #ECFEFF #164E63 #A5F3FC Cause-related colors + Trust + Warm
48 47 Music Streaming music, streaming #3B82F6 #60A5FA #F97316 #F8FAFC #1E293B #E2E8F0 Dark (#121212) + Vibrant accents + Album art colors
49 48 Video Streaming/OTT video, streaming, ott #3B82F6 #60A5FA #F97316 #F8FAFC #1E293B #E2E8F0 Dark bg + Content poster colors + Brand accent
50 49 Job Board/Recruitment job, board, recruitment #0F172A #334155 #0369A1 #F8FAFC #020617 #E2E8F0 Professional Blue + Success Green + Neutral
51 50 Marketplace (P2P) marketplace, p2p #3B82F6 #60A5FA #F97316 #F8FAFC #1E293B #E2E8F0 Trust colors + Category colors + Success green
52 51 Logistics/Delivery logistics, delivery #3B82F6 #60A5FA #F97316 #F8FAFC #1E293B #E2E8F0 Blue (#2563EB) + Orange (tracking) + Green (delivered)
53 52 Agriculture/Farm Tech agriculture, farm, tech #3B82F6 #60A5FA #F97316 #F8FAFC #1E293B #E2E8F0 Earth Green (#4A7C23) + Brown + Sky Blue
54 53 Construction/Architecture construction, architecture #3B82F6 #60A5FA #F97316 #F8FAFC #1E293B #E2E8F0 Grey (#4A4A4A) + Orange (safety) + Blueprint Blue
55 54 Automotive/Car Dealership automotive, car, dealership #3B82F6 #60A5FA #F97316 #F8FAFC #1E293B #E2E8F0 Brand colors + Metallic accents + Dark/Light
56 55 Photography Studio photography, studio #3B82F6 #60A5FA #F97316 #F8FAFC #1E293B #E2E8F0 Black + White + Minimal accent
57 56 Coworking Space coworking, space #3B82F6 #60A5FA #F97316 #F8FAFC #1E293B #E2E8F0 Energetic colors + Wood tones + Brand accent
58 57 Cleaning Service cleaning, service #3B82F6 #60A5FA #F97316 #F8FAFC #1E293B #E2E8F0 Fresh Blue (#00B4D8) + Clean White + Green
59 58 Home Services (Plumber/Electrician) home, services, plumber, electrician #0F172A #334155 #0369A1 #F8FAFC #020617 #E2E8F0 Trust Blue + Safety Orange + Professional grey
60 59 Childcare/Daycare childcare, daycare #3B82F6 #60A5FA #F97316 #F8FAFC #1E293B #E2E8F0 Playful pastels + Safe colors + Warm accents
61 60 Senior Care/Elderly senior, care, elderly #3B82F6 #60A5FA #F97316 #F8FAFC #1E293B #E2E8F0 Calm Blue + Warm neutrals + Large text
62 61 Medical Clinic medical, clinic #3B82F6 #60A5FA #F97316 #F8FAFC #1E293B #E2E8F0 Medical Blue (#0077B6) + Trust White + Calm Green
63 62 Pharmacy/Drug Store pharmacy, drug, store #3B82F6 #60A5FA #F97316 #F8FAFC #1E293B #E2E8F0 Pharmacy Green + Trust Blue + Clean White
64 63 Dental Practice dental, practice #3B82F6 #60A5FA #F97316 #F8FAFC #1E293B #E2E8F0 Fresh Blue + White + Smile Yellow accent
65 64 Veterinary Clinic veterinary, clinic #3B82F6 #60A5FA #F97316 #F8FAFC #1E293B #E2E8F0 Caring Blue + Pet-friendly colors + Warm accents
66 65 Florist/Plant Shop florist, plant, shop #3B82F6 #60A5FA #F97316 #F8FAFC #1E293B #E2E8F0 Natural Green + Floral pinks/purples + Earth tones
67 66 Bakery/Cafe bakery, cafe #3B82F6 #60A5FA #F97316 #F8FAFC #1E293B #E2E8F0 Warm Brown + Cream + Appetizing accents
68 67 Coffee Shop coffee, shop #3B82F6 #60A5FA #F97316 #F8FAFC #1E293B #E2E8F0 Coffee Brown (#6F4E37) + Cream + Warm accents
69 68 Brewery/Winery brewery, winery #3B82F6 #60A5FA #F97316 #F8FAFC #1E293B #E2E8F0 Deep amber/burgundy + Gold + Craft aesthetic
70 69 Airline airline #7C3AED #A78BFA #06B6D4 #FAF5FF #1E1B4B #DDD6FE Sky Blue + Brand colors + Trust accents
71 70 News/Media Platform news, media, platform #3B82F6 #60A5FA #F97316 #F8FAFC #1E293B #E2E8F0 Brand colors + High contrast + Category colors
72 71 Magazine/Blog magazine, blog #3B82F6 #60A5FA #F97316 #F8FAFC #1E293B #E2E8F0 Editorial colors + Brand primary + Clean white
73 72 Freelancer Platform freelancer, platform #0F172A #334155 #0369A1 #F8FAFC #020617 #E2E8F0 Professional Blue + Success Green + Neutral
74 73 Consulting Firm consulting, firm #0F172A #334155 #0369A1 #F8FAFC #020617 #E2E8F0 Navy + Gold + Professional grey
75 74 Marketing Agency marketing, agency #EC4899 #F472B6 #06B6D4 #FDF2F8 #831843 #FBCFE8 Bold brand colors + Creative freedom
76 75 Event Management event, management #7C3AED #A78BFA #F97316 #FAF5FF #4C1D95 #DDD6FE Event theme colors + Excitement accents
77 76 Conference/Webinar Platform conference, webinar, platform #0F172A #334155 #0369A1 #F8FAFC #020617 #E2E8F0 Professional Blue + Video accent + Brand
78 77 Membership/Community membership, community #7C3AED #A78BFA #F97316 #FAF5FF #4C1D95 #DDD6FE Community brand colors + Engagement accents
79 78 Newsletter Platform newsletter, platform #3B82F6 #60A5FA #F97316 #F8FAFC #1E293B #E2E8F0 Brand primary + Clean white + CTA accent
80 79 Digital Products/Downloads digital, products, downloads #3B82F6 #60A5FA #F97316 #F8FAFC #1E293B #E2E8F0 Product category colors + Brand + Success green
81 80 Church/Religious Organization church, religious, organization #3B82F6 #60A5FA #F97316 #F8FAFC #1E293B #E2E8F0 Warm Gold + Deep Purple/Blue + White
82 81 Sports Team/Club sports, team, club #3B82F6 #60A5FA #F97316 #F8FAFC #1E293B #E2E8F0 Team colors + Energetic accents
83 82 Museum/Gallery museum, gallery #3B82F6 #60A5FA #F97316 #F8FAFC #1E293B #E2E8F0 Art-appropriate neutrals + Exhibition accents
84 83 Theater/Cinema theater, cinema #3B82F6 #60A5FA #F97316 #F8FAFC #1E293B #E2E8F0 Dark + Spotlight accents + Gold
85 84 Language Learning App language, learning, app #0D9488 #2DD4BF #EA580C #F0FDFA #134E4A #5EEAD4 Playful colors + Progress indicators + Country flags
86 85 Coding Bootcamp coding, bootcamp #3B82F6 #60A5FA #F97316 #F8FAFC #1E293B #E2E8F0 Code editor colors + Brand + Success green
87 86 Cybersecurity Platform cybersecurity, security, cyber, hacker #00FF41 #0D0D0D #00FF41 #000000 #E0E0E0 #1F1F1F Matrix Green + Deep Black + Terminal feel
88 87 Developer Tool / IDE developer, tool, ide, code, dev #3B82F6 #1E293B #2563EB #0F172A #F1F5F9 #334155 Dark syntax theme colors + Blue focus
89 88 Biotech / Life Sciences biotech, science, biology, medical #0EA5E9 #0284C7 #10B981 #F8FAFC #0F172A #E2E8F0 Sterile White + DNA Blue + Life Green
90 89 Space Tech / Aerospace space, aerospace, tech, futuristic #FFFFFF #94A3B8 #3B82F6 #0B0B10 #F8FAFC #1E293B Deep Space Black + Star White + Metallic
91 90 Architecture / Interior architecture, interior, design, luxury #171717 #404040 #D4AF37 #FFFFFF #171717 #E5E5E5 Monochrome + Gold Accent + High Imagery
92 91 Quantum Computing quantum, qubit, tech #00FFFF #7B61FF #FF00FF #050510 #E0E0FF #333344 Interference patterns + Neon + Deep Dark
93 92 Biohacking / Longevity bio, health, science #FF4D4D #4D94FF #00E676 #F5F5F7 #1C1C1E #E5E5EA Biological red/blue + Clinical white
94 93 Autonomous Systems drone, robot, fleet #00FF41 #008F11 #FF3333 #0D1117 #E6EDF3 #30363D Terminal Green + Tactical Dark
95 94 Generative AI Art art, gen-ai, creative #111111 #333333 #FFFFFF #FAFAFA #000000 #E5E5E5 Canvas Neutral + High Contrast
96 95 Spatial / Vision OS spatial, glass, vision #FFFFFF #E5E5E5 #007AFF #888888 #000000 #FFFFFF Glass opacity 20% + System Blue
97 96 Climate Tech climate, green, energy #2E8B57 #87CEEB #FFD700 #F0FFF4 #1A3320 #C6E6C6 Nature Green + Solar Yellow + Air Blue

View File

@@ -1,31 +1,31 @@
No,Pattern Name,Keywords,Section Order,Primary CTA Placement,Color Strategy,Recommended Effects,Conversion Optimization No,Pattern Name,Keywords,Section Order,Primary CTA Placement,Color Strategy,Recommended Effects,Conversion Optimization
1,Hero + Features + CTA,"hero, hero-centric, features, feature-rich, cta, call-to-action","1. Hero with headline/image, 2. Value prop, 3. Key features (3-5), 4. CTA section, 5. Footer",Hero (sticky) + Bottom,Hero: Brand primary or vibrant. Features: Card bg #FAFAFA. CTA: Contrasting accent color,"Hero parallax, feature card hover lift, CTA glow on hover",Deep CTA placement. Use contrasting color (at least 7:1 contrast ratio). Sticky navbar CTA. 1,Hero + Features + CTA,"hero, hero-centric, features, feature-rich, cta, call-to-action","1. Hero with headline/image, 2. Value prop, 3. Key features (3-5), 4. CTA section, 5. Footer",Hero (sticky) + Bottom,Hero: Brand primary or vibrant. Features: Card bg #FAFAFA. CTA: Contrasting accent color,"Hero parallax, feature card hover lift, CTA glow on hover",Deep CTA placement. Use contrasting color (at least 7:1 contrast ratio). Sticky navbar CTA.
2,Hero + Testimonials + CTA,"hero, testimonials, social-proof, trust, reviews, cta","1. Hero, 2. Problem statement, 3. Solution overview, 4. Testimonials carousel, 5. CTA",Hero (sticky) + Post-testimonials,"Hero: Brand color. Testimonials: Light bg #F5F5F5. Quotes: Italic, muted color #666. CTA: Vibrant","Testimonial carousel slide animations, quote marks animations, avatar fade-in",Social proof before CTA. Use 3-5 testimonials. Include photo + name + role. CTA after social proof. 2,Hero + Testimonials + CTA,"hero, testimonials, social-proof, trust, reviews, cta","1. Hero, 2. Problem statement, 3. Solution overview, 4. Testimonials carousel, 5. CTA",Hero (sticky) + Post-testimonials,"Hero: Brand color. Testimonials: Light bg #F5F5F5. Quotes: Italic, muted color #666. CTA: Vibrant","Testimonial carousel slide animations, quote marks animations, avatar fade-in",Social proof before CTA. Use 3-5 testimonials. Include photo + name + role. CTA after social proof.
3,Product Demo + Features,"demo, product-demo, features, showcase, interactive","1. Hero, 2. Product video/mockup (center), 3. Feature breakdown per section, 4. Comparison (optional), 5. CTA",Video center + CTA right/bottom,Video surround: Brand color overlay. Features: Icon color #0080FF. Text: Dark #222,"Video play button pulse, feature scroll reveals, demo interaction highlights",Embedded product demo increases engagement. Use interactive mockup if possible. Auto-play video muted. 3,Product Demo + Features,"demo, product-demo, features, showcase, interactive","1. Hero, 2. Product video/mockup (center), 3. Feature breakdown per section, 4. Comparison (optional), 5. CTA",Video center + CTA right/bottom,Video surround: Brand color overlay. Features: Icon color #0080FF. Text: Dark #222,"Video play button pulse, feature scroll reveals, demo interaction highlights",Embedded product demo increases engagement. Use interactive mockup if possible. Auto-play video muted.
4,Minimal Single Column,"minimal, simple, direct, single-column, clean","1. Hero headline, 2. Short description, 3. Benefit bullets (3 max), 4. CTA, 5. Footer","Center, large CTA button",Minimalist: Brand + white #FFFFFF + accent. Buttons: High contrast 7:1+. Text: Black/Dark grey,Minimal hover effects. Smooth scroll. CTA scale on hover (subtle),Single CTA focus. Large typography. Lots of whitespace. No nav clutter. Mobile-first. 4,Minimal Single Column,"minimal, simple, direct, single-column, clean","1. Hero headline, 2. Short description, 3. Benefit bullets (3 max), 4. CTA, 5. Footer","Center, large CTA button",Minimalist: Brand + white #FFFFFF + accent. Buttons: High contrast 7:1+. Text: Black/Dark grey,Minimal hover effects. Smooth scroll. CTA scale on hover (subtle),Single CTA focus. Large typography. Lots of whitespace. No nav clutter. Mobile-first.
5,Funnel (3-Step Conversion),"funnel, conversion, steps, wizard, onboarding","1. Hero, 2. Step 1 (problem), 3. Step 2 (solution), 4. Step 3 (action), 5. CTA progression",Each step: mini-CTA. Final: main CTA,"Step colors: 1 (Red/Problem), 2 (Orange/Process), 3 (Green/Solution). CTA: Brand color","Step number animations, progress bar fill, step transitions smooth scroll",Progressive disclosure. Show only essential info per step. Use progress indicators. Multiple CTAs. 5,Funnel (3-Step Conversion),"funnel, conversion, steps, wizard, onboarding","1. Hero, 2. Step 1 (problem), 3. Step 2 (solution), 4. Step 3 (action), 5. CTA progression",Each step: mini-CTA. Final: main CTA,"Step colors: 1 (Red/Problem), 2 (Orange/Process), 3 (Green/Solution). CTA: Brand color","Step number animations, progress bar fill, step transitions smooth scroll",Progressive disclosure. Show only essential info per step. Use progress indicators. Multiple CTAs.
6,Comparison Table + CTA,"comparison, table, compare, versus, cta","1. Hero, 2. Problem intro, 3. Comparison table (product vs competitors), 4. Pricing (optional), 5. CTA",Table: Right column. CTA: Below table,Table: Alternating rows (white/light grey). Your product: Highlight #FFFACD (light yellow) or green. Text: Dark,"Table row hover highlight, price toggle animations, feature checkmark animations",Use comparison to show unique value. Highlight your product row. Include 'free trial' in pricing row. 6,Comparison Table + CTA,"comparison, table, compare, versus, cta","1. Hero, 2. Problem intro, 3. Comparison table (product vs competitors), 4. Pricing (optional), 5. CTA",Table: Right column. CTA: Below table,Table: Alternating rows (white/light grey). Your product: Highlight #FFFACD (light yellow) or green. Text: Dark,"Table row hover highlight, price toggle animations, feature checkmark animations",Use comparison to show unique value. Highlight your product row. Include 'free trial' in pricing row.
7,Lead Magnet + Form,"lead, form, signup, capture, email, magnet","1. Hero (benefit headline), 2. Lead magnet preview (ebook cover, checklist, etc), 3. Form (minimal fields), 4. CTA submit",Form CTA: Submit button,Lead magnet: Professional design. Form: Clean white bg. Inputs: Light border #CCCCCC. CTA: Brand color,"Form focus state animations, input validation animations, success confirmation animation",Form fields ≤ 3 for best conversion. Offer valuable lead magnet preview. Show form submission progress. 7,Lead Magnet + Form,"lead, form, signup, capture, email, magnet","1. Hero (benefit headline), 2. Lead magnet preview (ebook cover, checklist, etc), 3. Form (minimal fields), 4. CTA submit",Form CTA: Submit button,Lead magnet: Professional design. Form: Clean white bg. Inputs: Light border #CCCCCC. CTA: Brand color,"Form focus state animations, input validation animations, success confirmation animation",Form fields ≤ 3 for best conversion. Offer valuable lead magnet preview. Show form submission progress.
8,Pricing Page + CTA,"pricing, plans, tiers, comparison, cta","1. Hero (pricing headline), 2. Price comparison cards, 3. Feature comparison table, 4. FAQ section, 5. Final CTA",Each card: CTA button. Sticky CTA in nav,"Free: Grey, Starter: Blue, Pro: Green/Gold, Enterprise: Dark. Cards: 1px border, shadow","Price toggle animation (monthly/yearly), card comparison highlight, FAQ accordion open/close",Recommend starter plan (pre-select/highlight). Show annual discount (20-30%). Use FAQs to address concerns. 8,Pricing Page + CTA,"pricing, plans, tiers, comparison, cta","1. Hero (pricing headline), 2. Price comparison cards, 3. Feature comparison table, 4. FAQ section, 5. Final CTA",Each card: CTA button. Sticky CTA in nav,"Free: Grey, Starter: Blue, Pro: Green/Gold, Enterprise: Dark. Cards: 1px border, shadow","Price toggle animation (monthly/yearly), card comparison highlight, FAQ accordion open/close",Recommend starter plan (pre-select/highlight). Show annual discount (20-30%). Use FAQs to address concerns.
9,Video-First Hero,"video, hero, media, visual, engaging","1. Hero with video background, 2. Key features overlay, 3. Benefits section, 4. CTA",Overlay on video (center/bottom) + Bottom section,Dark overlay 60% on video. Brand accent for CTA. White text on dark.,"Video autoplay muted, parallax scroll, text fade-in on scroll",86% higher engagement with video. Add captions for accessibility. Compress video for performance. 9,Video-First Hero,"video, hero, media, visual, engaging","1. Hero with video background, 2. Key features overlay, 3. Benefits section, 4. CTA",Overlay on video (center/bottom) + Bottom section,Dark overlay 60% on video. Brand accent for CTA. White text on dark.,"Video autoplay muted, parallax scroll, text fade-in on scroll",86% higher engagement with video. Add captions for accessibility. Compress video for performance.
10,Scroll-Triggered Storytelling,"storytelling, scroll, narrative, story, immersive","1. Intro hook, 2. Chapter 1 (problem), 3. Chapter 2 (journey), 4. Chapter 3 (solution), 5. Climax CTA",End of each chapter (mini) + Final climax CTA,Progressive reveal. Each chapter has distinct color. Building intensity.,"ScrollTrigger animations, parallax layers, progressive disclosure, chapter transitions",Narrative increases time-on-page 3x. Use progress indicator. Mobile: simplify animations. 10,Scroll-Triggered Storytelling,"storytelling, scroll, narrative, story, immersive","1. Intro hook, 2. Chapter 1 (problem), 3. Chapter 2 (journey), 4. Chapter 3 (solution), 5. Climax CTA",End of each chapter (mini) + Final climax CTA,Progressive reveal. Each chapter has distinct color. Building intensity.,"ScrollTrigger animations, parallax layers, progressive disclosure, chapter transitions",Narrative increases time-on-page 3x. Use progress indicator. Mobile: simplify animations.
11,AI Personalization Landing,"ai, personalization, smart, recommendation, dynamic","1. Dynamic hero (personalized), 2. Relevant features, 3. Tailored testimonials, 4. Smart CTA",Context-aware placement based on user segment,Adaptive based on user data. A/B test color variations per segment.,"Dynamic content swap, fade transitions, personalized product recommendations",20%+ conversion with personalization. Requires analytics integration. Fallback for new users. 11,AI Personalization Landing,"ai, personalization, smart, recommendation, dynamic","1. Dynamic hero (personalized), 2. Relevant features, 3. Tailored testimonials, 4. Smart CTA",Context-aware placement based on user segment,Adaptive based on user data. A/B test color variations per segment.,"Dynamic content swap, fade transitions, personalized product recommendations",20%+ conversion with personalization. Requires analytics integration. Fallback for new users.
12,Waitlist/Coming Soon,"waitlist, coming-soon, launch, early-access, notify","1. Hero with countdown, 2. Product teaser/preview, 3. Email capture form, 4. Social proof (waitlist count)",Email form prominent (above fold) + Sticky form on scroll,Anticipation: Dark + accent highlights. Countdown in brand color. Urgency indicators.,"Countdown timer animation, email validation feedback, success confetti, social share buttons",Scarcity + exclusivity. Show waitlist count. Early access benefits. Referral program. 12,Waitlist/Coming Soon,"waitlist, coming-soon, launch, early-access, notify","1. Hero with countdown, 2. Product teaser/preview, 3. Email capture form, 4. Social proof (waitlist count)",Email form prominent (above fold) + Sticky form on scroll,Anticipation: Dark + accent highlights. Countdown in brand color. Urgency indicators.,"Countdown timer animation, email validation feedback, success confetti, social share buttons",Scarcity + exclusivity. Show waitlist count. Early access benefits. Referral program.
13,Comparison Table Focus,"comparison, table, versus, compare, features","1. Hero (problem statement), 2. Comparison matrix (you vs competitors), 3. Feature deep-dive, 4. Winner CTA",After comparison table (highlighted row) + Bottom,Your product column highlighted (accent bg or green). Competitors neutral. Checkmarks green.,"Table row hover highlight, feature checkmark animations, sticky comparison header",Show value vs competitors. 35% higher conversion. Be factual. Include pricing if favorable. 13,Comparison Table Focus,"comparison, table, versus, compare, features","1. Hero (problem statement), 2. Comparison matrix (you vs competitors), 3. Feature deep-dive, 4. Winner CTA",After comparison table (highlighted row) + Bottom,Your product column highlighted (accent bg or green). Competitors neutral. Checkmarks green.,"Table row hover highlight, feature checkmark animations, sticky comparison header",Show value vs competitors. 35% higher conversion. Be factual. Include pricing if favorable.
14,Pricing-Focused Landing,"pricing, price, cost, plans, subscription","1. Hero (value proposition), 2. Pricing cards (3 tiers), 3. Feature comparison, 4. FAQ, 5. Final CTA",Each pricing card + Sticky CTA in nav + Bottom,Popular plan highlighted (brand color border/bg). Free: grey. Enterprise: dark/premium.,"Price toggle monthly/annual animation, card hover lift, FAQ accordion smooth open",Annual discount 20-30%. Recommend mid-tier (most popular badge). Address objections in FAQ. 14,Pricing-Focused Landing,"pricing, price, cost, plans, subscription","1. Hero (value proposition), 2. Pricing cards (3 tiers), 3. Feature comparison, 4. FAQ, 5. Final CTA",Each pricing card + Sticky CTA in nav + Bottom,Popular plan highlighted (brand color border/bg). Free: grey. Enterprise: dark/premium.,"Price toggle monthly/annual animation, card hover lift, FAQ accordion smooth open",Annual discount 20-30%. Recommend mid-tier (most popular badge). Address objections in FAQ.
15,App Store Style Landing,"app, mobile, download, store, install","1. Hero with device mockup, 2. Screenshots carousel, 3. Features with icons, 4. Reviews/ratings, 5. Download CTAs",Download buttons prominent (App Store + Play Store) throughout,Dark/light matching app store feel. Star ratings in gold. Screenshots with device frames.,"Device mockup rotations, screenshot slider, star rating animations, download button pulse",Show real screenshots. Include ratings (4.5+ stars). QR code for mobile. Platform-specific CTAs. 15,App Store Style Landing,"app, mobile, download, store, install","1. Hero with device mockup, 2. Screenshots carousel, 3. Features with icons, 4. Reviews/ratings, 5. Download CTAs",Download buttons prominent (App Store + Play Store) throughout,Dark/light matching app store feel. Star ratings in gold. Screenshots with device frames.,"Device mockup rotations, screenshot slider, star rating animations, download button pulse",Show real screenshots. Include ratings (4.5+ stars). QR code for mobile. Platform-specific CTAs.
16,FAQ/Documentation Landing,"faq, documentation, help, support, questions","1. Hero with search bar, 2. Popular categories, 3. FAQ accordion, 4. Contact/support CTA",Search bar prominent + Contact CTA for unresolved questions,"Clean, high readability. Minimal color. Category icons in brand color. Success green for resolved.","Search autocomplete, smooth accordion open/close, category hover, helpful feedback buttons",Reduce support tickets. Track search analytics. Show related articles. Contact escalation path. 16,FAQ/Documentation Landing,"faq, documentation, help, support, questions","1. Hero with search bar, 2. Popular categories, 3. FAQ accordion, 4. Contact/support CTA",Search bar prominent + Contact CTA for unresolved questions,"Clean, high readability. Minimal color. Category icons in brand color. Success green for resolved.","Search autocomplete, smooth accordion open/close, category hover, helpful feedback buttons",Reduce support tickets. Track search analytics. Show related articles. Contact escalation path.
17,Immersive/Interactive Experience,"immersive, interactive, experience, 3d, animation","1. Full-screen interactive element, 2. Guided product tour, 3. Key benefits revealed, 4. CTA after completion",After interaction complete + Skip option for impatient users,Immersive experience colors. Dark background for focus. Highlight interactive elements.,"WebGL, 3D interactions, gamification elements, progress indicators, reward animations",40% higher engagement. Performance trade-off. Provide skip option. Mobile fallback essential. 17,Immersive/Interactive Experience,"immersive, interactive, experience, 3d, animation","1. Full-screen interactive element, 2. Guided product tour, 3. Key benefits revealed, 4. CTA after completion",After interaction complete + Skip option for impatient users,Immersive experience colors. Dark background for focus. Highlight interactive elements.,"WebGL, 3D interactions, gamification elements, progress indicators, reward animations",40% higher engagement. Performance trade-off. Provide skip option. Mobile fallback essential.
18,Event/Conference Landing,"event, conference, meetup, registration, schedule","1. Hero (date/location/countdown), 2. Speakers grid, 3. Agenda/schedule, 4. Sponsors, 5. Register CTA",Register CTA sticky + After speakers + Bottom,Urgency colors (countdown). Event branding. Speaker cards professional. Sponsor logos neutral.,"Countdown timer, speaker hover cards with bio, agenda tabs, early bird countdown",Early bird pricing with deadline. Social proof (past attendees). Speaker credibility. Multi-ticket discounts. 18,Event/Conference Landing,"event, conference, meetup, registration, schedule","1. Hero (date/location/countdown), 2. Speakers grid, 3. Agenda/schedule, 4. Sponsors, 5. Register CTA",Register CTA sticky + After speakers + Bottom,Urgency colors (countdown). Event branding. Speaker cards professional. Sponsor logos neutral.,"Countdown timer, speaker hover cards with bio, agenda tabs, early bird countdown",Early bird pricing with deadline. Social proof (past attendees). Speaker credibility. Multi-ticket discounts.
19,Product Review/Ratings Focused,"reviews, ratings, testimonials, social-proof, stars","1. Hero (product + aggregate rating), 2. Rating breakdown, 3. Individual reviews, 4. Buy/CTA",After reviews summary + Buy button alongside reviews,Trust colors. Star ratings gold. Verified badge green. Review sentiment colors.,"Star fill animations, review filtering, helpful vote interactions, photo lightbox",User-generated content builds trust. Show verified purchases. Filter by rating. Respond to negative reviews. 19,Product Review/Ratings Focused,"reviews, ratings, testimonials, social-proof, stars","1. Hero (product + aggregate rating), 2. Rating breakdown, 3. Individual reviews, 4. Buy/CTA",After reviews summary + Buy button alongside reviews,Trust colors. Star ratings gold. Verified badge green. Review sentiment colors.,"Star fill animations, review filtering, helpful vote interactions, photo lightbox",User-generated content builds trust. Show verified purchases. Filter by rating. Respond to negative reviews.
20,Community/Forum Landing,"community, forum, social, members, discussion","1. Hero (community value prop), 2. Popular topics/categories, 3. Active members showcase, 4. Join CTA",Join button prominent + After member showcase,"Warm, welcoming. Member photos add humanity. Topic badges in brand colors. Activity indicators green.","Member avatars animation, activity feed live updates, topic hover previews, join success celebration","Show active community (member count, posts today). Highlight benefits. Preview content. Easy onboarding." 20,Community/Forum Landing,"community, forum, social, members, discussion","1. Hero (community value prop), 2. Popular topics/categories, 3. Active members showcase, 4. Join CTA",Join button prominent + After member showcase,"Warm, welcoming. Member photos add humanity. Topic badges in brand colors. Activity indicators green.","Member avatars animation, activity feed live updates, topic hover previews, join success celebration","Show active community (member count, posts today). Highlight benefits. Preview content. Easy onboarding."
21,Before-After Transformation,"before-after, transformation, results, comparison","1. Hero (problem state), 2. Transformation slider/comparison, 3. How it works, 4. Results CTA",After transformation reveal + Bottom,Contrast: muted/grey (before) vs vibrant/colorful (after). Success green for results.,"Slider comparison interaction, before/after reveal animations, result counters, testimonial videos",Visual proof of value. 45% higher conversion. Real results. Specific metrics. Guarantee offer. 21,Before-After Transformation,"before-after, transformation, results, comparison","1. Hero (problem state), 2. Transformation slider/comparison, 3. How it works, 4. Results CTA",After transformation reveal + Bottom,Contrast: muted/grey (before) vs vibrant/colorful (after). Success green for results.,"Slider comparison interaction, before/after reveal animations, result counters, testimonial videos",Visual proof of value. 45% higher conversion. Real results. Specific metrics. Guarantee offer.
22,Marketplace / Directory,"marketplace, directory, search, listing","1. Hero (Search focused), 2. Categories, 3. Featured Listings, 4. Trust/Safety, 5. CTA (Become a host/seller)",Hero Search Bar + Navbar 'List your item',Search: High contrast. Categories: Visual icons. Trust: Blue/Green.,Search autocomplete animation, map hover pins, card carousel,Search bar is the CTA. Reduce friction to search. Popular searches suggestions. 22,Marketplace / Directory,"marketplace, directory, search, listing","1. Hero (Search focused), 2. Categories, 3. Featured Listings, 4. Trust/Safety, 5. CTA (Become a host/seller)",Hero Search Bar + Navbar 'List your item',Search: High contrast. Categories: Visual icons. Trust: Blue/Green.,Search autocomplete animation, map hover pins, card carousel,Search bar is the CTA. Reduce friction to search. Popular searches suggestions.
23,Newsletter / Content First,"newsletter, content, writer, blog, subscribe","1. Hero (Value Prop + Form), 2. Recent Issues/Archives, 3. Social Proof (Subscriber count), 4. About Author",Hero inline form + Sticky header form,Minimalist. Paper-like background. Text focus. Accent color for Subscribe.,Text highlight animations, typewriter effect, subtle fade-in,Single field form (Email only). Show 'Join X,000 readers'. Read sample link. 23,Newsletter / Content First,"newsletter, content, writer, blog, subscribe","1. Hero (Value Prop + Form), 2. Recent Issues/Archives, 3. Social Proof (Subscriber count), 4. About Author",Hero inline form + Sticky header form,Minimalist. Paper-like background. Text focus. Accent color for Subscribe.,Text highlight animations, typewriter effect, subtle fade-in,Single field form (Email only). Show 'Join X,000 readers'. Read sample link.
24,Webinar Registration,"webinar, registration, event, training, live","1. Hero (Topic + Timer + Form), 2. What you'll learn, 3. Speaker Bio, 4. Urgency/Bonuses, 5. Form (again)",Hero (Right side form) + Bottom anchor,Urgency: Red/Orange. Professional: Blue/Navy. Form: High contrast white.,Countdown timer, speaker avatar float, urgent ticker,Limited seats logic. 'Live' indicator. Auto-fill timezone. 24,Webinar Registration,"webinar, registration, event, training, live","1. Hero (Topic + Timer + Form), 2. What you'll learn, 3. Speaker Bio, 4. Urgency/Bonuses, 5. Form (again)",Hero (Right side form) + Bottom anchor,Urgency: Red/Orange. Professional: Blue/Navy. Form: High contrast white.,Countdown timer, speaker avatar float, urgent ticker,Limited seats logic. 'Live' indicator. Auto-fill timezone.
25,Enterprise Gateway,"enterprise, corporate, gateway, solutions, portal","1. Hero (Video/Mission), 2. Solutions by Industry, 3. Solutions by Role, 4. Client Logos, 5. Contact Sales",Contact Sales (Primary) + Login (Secondary),Corporate: Navy/Grey. High integrity. Conservative accents.,Slow video background, logo carousel, tab switching for industries,Path selection (I am a...). Mega menu navigation. Trust signals prominent. 25,Enterprise Gateway,"enterprise, corporate, gateway, solutions, portal","1. Hero (Video/Mission), 2. Solutions by Industry, 3. Solutions by Role, 4. Client Logos, 5. Contact Sales",Contact Sales (Primary) + Login (Secondary),Corporate: Navy/Grey. High integrity. Conservative accents.,Slow video background, logo carousel, tab switching for industries,Path selection (I am a...). Mega menu navigation. Trust signals prominent.
26,Portfolio Grid,"portfolio, grid, showcase, gallery, masonry","1. Hero (Name/Role), 2. Project Grid (Masonry), 3. About/Philosophy, 4. Contact",Project Card Hover + Footer Contact,Neutral background (let work shine). Text: Black/White. Accent: Minimal.,Image lazy load reveal, hover overlay info, lightbox view,Visuals first. Filter by category. Fast loading essential. 26,Portfolio Grid,"portfolio, grid, showcase, gallery, masonry","1. Hero (Name/Role), 2. Project Grid (Masonry), 3. About/Philosophy, 4. Contact",Project Card Hover + Footer Contact,Neutral background (let work shine). Text: Black/White. Accent: Minimal.,Image lazy load reveal, hover overlay info, lightbox view,Visuals first. Filter by category. Fast loading essential.
27,Horizontal Scroll Journey,"horizontal, scroll, journey, gallery, storytelling, panoramic","1. Intro (Vertical), 2. The Journey (Horizontal Track), 3. Detail Reveal, 4. Vertical Footer","Floating Sticky CTA or End of Horizontal Track","Continuous palette transition. Chapter colors. Progress bar #000000.","Scroll-jacking (careful), parallax layers, horizontal slide, progress indicator","Immersive product discovery. High engagement. Keep navigation visible. 27,Horizontal Scroll Journey,"horizontal, scroll, journey, gallery, storytelling, panoramic","1. Intro (Vertical), 2. The Journey (Horizontal Track), 3. Detail Reveal, 4. Vertical Footer","Floating Sticky CTA or End of Horizontal Track","Continuous palette transition. Chapter colors. Progress bar #000000.","Scroll-jacking (careful), parallax layers, horizontal slide, progress indicator","Immersive product discovery. High engagement. Keep navigation visible.
28,Bento Grid Showcase,"bento, grid, features, modular, apple-style, showcase","1. Hero, 2. Bento Grid (Key Features), 3. Detail Cards, 4. Tech Specs, 5. CTA","Floating Action Button or Bottom of Grid","Card backgrounds: #F5F5F7 or Glass. Icons: Vibrant brand colors. Text: Dark.","Hover card scale (1.02), video inside cards, tilt effect, staggered reveal","Scannable value props. High information density without clutter. Mobile stack. 28,Bento Grid Showcase,"bento, grid, features, modular, apple-style, showcase","1. Hero, 2. Bento Grid (Key Features), 3. Detail Cards, 4. Tech Specs, 5. CTA","Floating Action Button or Bottom of Grid","Card backgrounds: #F5F5F7 or Glass. Icons: Vibrant brand colors. Text: Dark.","Hover card scale (1.02), video inside cards, tilt effect, staggered reveal","Scannable value props. High information density without clutter. Mobile stack.
29,Interactive 3D Configurator,"3d, configurator, customizer, interactive, product","1. Hero (Configurator), 2. Feature Highlight (synced), 3. Price/Specs, 4. Purchase","Inside Configurator UI + Sticky Bottom Bar","Neutral studio background. Product: Realistic materials. UI: Minimal overlay.","Real-time rendering, material swap animation, camera rotate/zoom, light reflection","Increases ownership feeling. 360 view reduces return rates. Direct add-to-cart. 29,Interactive 3D Configurator,"3d, configurator, customizer, interactive, product","1. Hero (Configurator), 2. Feature Highlight (synced), 3. Price/Specs, 4. Purchase","Inside Configurator UI + Sticky Bottom Bar","Neutral studio background. Product: Realistic materials. UI: Minimal overlay.","Real-time rendering, material swap animation, camera rotate/zoom, light reflection","Increases ownership feeling. 360 view reduces return rates. Direct add-to-cart.
30,AI-Driven Dynamic Landing,"ai, dynamic, personalized, adaptive, generative","1. Prompt/Input Hero, 2. Generated Result Preview, 3. How it Works, 4. Value Prop","Input Field (Hero) + 'Try it' Buttons","Adaptive to user input. Dark mode for compute feel. Neon accents.","Typing text effects, shimmering generation loaders, morphing layouts","Immediate value demonstration. 'Show, don't tell'. Low friction start. 30,AI-Driven Dynamic Landing,"ai, dynamic, personalized, adaptive, generative","1. Prompt/Input Hero, 2. Generated Result Preview, 3. How it Works, 4. Value Prop","Input Field (Hero) + 'Try it' Buttons","Adaptive to user input. Dark mode for compute feel. Neon accents.","Typing text effects, shimmering generation loaders, morphing layouts","Immediate value demonstration. 'Show, don't tell'. Low friction start.
Can't render this file because it contains an unexpected character in line 29 and column 24.

View File

@@ -1,97 +1,97 @@
No,Product Type,Keywords,Primary Style Recommendation,Secondary Styles,Landing Page Pattern,Dashboard Style (if applicable),Color Palette Focus,Key Considerations No,Product Type,Keywords,Primary Style Recommendation,Secondary Styles,Landing Page Pattern,Dashboard Style (if applicable),Color Palette Focus,Key Considerations
1,SaaS (General),"app, b2b, cloud, general, saas, software, subscription",Glassmorphism + Flat Design,"Soft UI Evolution, Minimalism",Hero + Features + CTA,Data-Dense + Real-Time Monitoring,Trust blue + accent contrast,Balance modern feel with clarity. Focus on CTAs. 1,SaaS (General),"app, b2b, cloud, general, saas, software, subscription",Glassmorphism + Flat Design,"Soft UI Evolution, Minimalism",Hero + Features + CTA,Data-Dense + Real-Time Monitoring,Trust blue + accent contrast,Balance modern feel with clarity. Focus on CTAs.
2,Micro SaaS,"app, b2b, cloud, indie, micro, micro-saas, niche, saas, small, software, solo, subscription",Flat Design + Vibrant & Block,"Motion-Driven, Micro-interactions",Minimal & Direct + Demo,Executive Dashboard,Vibrant primary + white space,"Keep simple, show product quickly. Speed is key." 2,Micro SaaS,"app, b2b, cloud, indie, micro, micro-saas, niche, saas, small, software, solo, subscription",Flat Design + Vibrant & Block,"Motion-Driven, Micro-interactions",Minimal & Direct + Demo,Executive Dashboard,Vibrant primary + white space,"Keep simple, show product quickly. Speed is key."
3,E-commerce,"buy, commerce, e, ecommerce, products, retail, sell, shop, store",Vibrant & Block-based,"Aurora UI, Motion-Driven",Feature-Rich Showcase,Sales Intelligence Dashboard,Brand primary + success green,Engagement & conversions. High visual hierarchy. 3,E-commerce,"buy, commerce, e, ecommerce, products, retail, sell, shop, store",Vibrant & Block-based,"Aurora UI, Motion-Driven",Feature-Rich Showcase,Sales Intelligence Dashboard,Brand primary + success green,Engagement & conversions. High visual hierarchy.
4,E-commerce Luxury,"buy, commerce, e, ecommerce, elegant, exclusive, high-end, luxury, premium, products, retail, sell, shop, store",Liquid Glass + Glassmorphism,"3D & Hyperrealism, Aurora UI",Feature-Rich Showcase,Sales Intelligence Dashboard,Premium colors + minimal accent,Elegance & sophistication. Premium materials. 4,E-commerce Luxury,"buy, commerce, e, ecommerce, elegant, exclusive, high-end, luxury, premium, products, retail, sell, shop, store",Liquid Glass + Glassmorphism,"3D & Hyperrealism, Aurora UI",Feature-Rich Showcase,Sales Intelligence Dashboard,Premium colors + minimal accent,Elegance & sophistication. Premium materials.
5,Service Landing Page,"appointment, booking, consultation, conversion, landing, marketing, page, service",Hero-Centric + Trust & Authority,"Social Proof-Focused, Storytelling",Hero-Centric Design,N/A - Analytics for conversions,Brand primary + trust colors,Social proof essential. Show expertise. 5,Service Landing Page,"appointment, booking, consultation, conversion, landing, marketing, page, service",Hero-Centric + Trust & Authority,"Social Proof-Focused, Storytelling",Hero-Centric Design,N/A - Analytics for conversions,Brand primary + trust colors,Social proof essential. Show expertise.
6,B2B Service,"appointment, b, b2b, booking, business, consultation, corporate, enterprise, service",Trust & Authority + Minimal,"Feature-Rich, Conversion-Optimized",Feature-Rich Showcase,Sales Intelligence Dashboard,Professional blue + neutral grey,Credibility essential. Clear ROI messaging. 6,B2B Service,"appointment, b, b2b, booking, business, consultation, corporate, enterprise, service",Trust & Authority + Minimal,"Feature-Rich, Conversion-Optimized",Feature-Rich Showcase,Sales Intelligence Dashboard,Professional blue + neutral grey,Credibility essential. Clear ROI messaging.
7,Financial Dashboard,"admin, analytics, dashboard, data, financial, panel",Dark Mode (OLED) + Data-Dense,"Minimalism, Accessible & Ethical",N/A - Dashboard focused,Financial Dashboard,Dark bg + red/green alerts + trust blue,"High contrast, real-time updates, accuracy paramount." 7,Financial Dashboard,"admin, analytics, dashboard, data, financial, panel",Dark Mode (OLED) + Data-Dense,"Minimalism, Accessible & Ethical",N/A - Dashboard focused,Financial Dashboard,Dark bg + red/green alerts + trust blue,"High contrast, real-time updates, accuracy paramount."
8,Analytics Dashboard,"admin, analytics, dashboard, data, panel",Data-Dense + Heat Map & Heatmap,"Minimalism, Dark Mode (OLED)",N/A - Analytics focused,Drill-Down Analytics + Comparative,Cool→Hot gradients + neutral grey,Clarity > aesthetics. Color-coded data priority. 8,Analytics Dashboard,"admin, analytics, dashboard, data, panel",Data-Dense + Heat Map & Heatmap,"Minimalism, Dark Mode (OLED)",N/A - Analytics focused,Drill-Down Analytics + Comparative,Cool→Hot gradients + neutral grey,Clarity > aesthetics. Color-coded data priority.
9,Healthcare App,"app, clinic, health, healthcare, medical, patient",Neumorphism + Accessible & Ethical,"Soft UI Evolution, Claymorphism (for patients)",Social Proof-Focused,User Behavior Analytics,Calm blue + health green + trust,Accessibility mandatory. Calming aesthetic. 9,Healthcare App,"app, clinic, health, healthcare, medical, patient",Neumorphism + Accessible & Ethical,"Soft UI Evolution, Claymorphism (for patients)",Social Proof-Focused,User Behavior Analytics,Calm blue + health green + trust,Accessibility mandatory. Calming aesthetic.
10,Educational App,"app, course, education, educational, learning, school, training",Claymorphism + Micro-interactions,"Vibrant & Block-based, Flat Design",Storytelling-Driven,User Behavior Analytics,Playful colors + clear hierarchy,Engagement & ease of use. Age-appropriate design. 10,Educational App,"app, course, education, educational, learning, school, training",Claymorphism + Micro-interactions,"Vibrant & Block-based, Flat Design",Storytelling-Driven,User Behavior Analytics,Playful colors + clear hierarchy,Engagement & ease of use. Age-appropriate design.
11,Creative Agency,"agency, creative, design, marketing, studio",Brutalism + Motion-Driven,"Retro-Futurism, Storytelling-Driven",Storytelling-Driven,N/A - Portfolio focused,Bold primaries + artistic freedom,Differentiation key. Wow-factor necessary. 11,Creative Agency,"agency, creative, design, marketing, studio",Brutalism + Motion-Driven,"Retro-Futurism, Storytelling-Driven",Storytelling-Driven,N/A - Portfolio focused,Bold primaries + artistic freedom,Differentiation key. Wow-factor necessary.
12,Portfolio/Personal,"creative, personal, portfolio, projects, showcase, work",Motion-Driven + Minimalism,"Brutalism, Aurora UI",Storytelling-Driven,N/A - Personal branding,Brand primary + artistic interpretation,Showcase work. Personality shine through. 12,Portfolio/Personal,"creative, personal, portfolio, projects, showcase, work",Motion-Driven + Minimalism,"Brutalism, Aurora UI",Storytelling-Driven,N/A - Personal branding,Brand primary + artistic interpretation,Showcase work. Personality shine through.
13,Gaming,"entertainment, esports, game, gaming, play",3D & Hyperrealism + Retro-Futurism,"Motion-Driven, Vibrant & Block",Feature-Rich Showcase,N/A - Game focused,Vibrant + neon + immersive colors,Immersion priority. Performance critical. 13,Gaming,"entertainment, esports, game, gaming, play",3D & Hyperrealism + Retro-Futurism,"Motion-Driven, Vibrant & Block",Feature-Rich Showcase,N/A - Game focused,Vibrant + neon + immersive colors,Immersion priority. Performance critical.
14,Government/Public Service,"appointment, booking, consultation, government, public, service",Accessible & Ethical + Minimalism,"Flat Design, Inclusive Design",Minimal & Direct,Executive Dashboard,Professional blue + high contrast,WCAG AAA mandatory. Trust paramount. 14,Government/Public Service,"appointment, booking, consultation, government, public, service",Accessible & Ethical + Minimalism,"Flat Design, Inclusive Design",Minimal & Direct,Executive Dashboard,Professional blue + high contrast,WCAG AAA mandatory. Trust paramount.
15,Fintech/Crypto,"banking, blockchain, crypto, defi, finance, fintech, money, nft, payment, web3",Glassmorphism + Dark Mode (OLED),"Retro-Futurism, Motion-Driven",Conversion-Optimized,Real-Time Monitoring + Predictive,Dark tech colors + trust + vibrant accents,Security perception. Real-time data critical. 15,Fintech/Crypto,"banking, blockchain, crypto, defi, finance, fintech, money, nft, payment, web3",Glassmorphism + Dark Mode (OLED),"Retro-Futurism, Motion-Driven",Conversion-Optimized,Real-Time Monitoring + Predictive,Dark tech colors + trust + vibrant accents,Security perception. Real-time data critical.
16,Social Media App,"app, community, content, entertainment, media, network, sharing, social, streaming, users, video",Vibrant & Block-based + Motion-Driven,"Aurora UI, Micro-interactions",Feature-Rich Showcase,User Behavior Analytics,Vibrant + engagement colors,Engagement & retention. Addictive design ethics. 16,Social Media App,"app, community, content, entertainment, media, network, sharing, social, streaming, users, video",Vibrant & Block-based + Motion-Driven,"Aurora UI, Micro-interactions",Feature-Rich Showcase,User Behavior Analytics,Vibrant + engagement colors,Engagement & retention. Addictive design ethics.
17,Productivity Tool,"collaboration, productivity, project, task, tool, workflow",Flat Design + Micro-interactions,"Minimalism, Soft UI Evolution",Interactive Product Demo,Drill-Down Analytics,Clear hierarchy + functional colors,Ease of use. Speed & efficiency focus. 17,Productivity Tool,"collaboration, productivity, project, task, tool, workflow",Flat Design + Micro-interactions,"Minimalism, Soft UI Evolution",Interactive Product Demo,Drill-Down Analytics,Clear hierarchy + functional colors,Ease of use. Speed & efficiency focus.
18,Design System/Component Library,"component, design, library, system",Minimalism + Accessible & Ethical,"Flat Design, Zero Interface",Feature-Rich Showcase,N/A - Dev focused,Clear hierarchy + code-like structure,Consistency. Developer-first approach. 18,Design System/Component Library,"component, design, library, system",Minimalism + Accessible & Ethical,"Flat Design, Zero Interface",Feature-Rich Showcase,N/A - Dev focused,Clear hierarchy + code-like structure,Consistency. Developer-first approach.
19,AI/Chatbot Platform,"ai, artificial-intelligence, automation, chatbot, machine-learning, ml, platform",AI-Native UI + Minimalism,"Zero Interface, Glassmorphism",Interactive Product Demo,AI/ML Analytics Dashboard,Neutral + AI Purple (#6366F1),Conversational UI. Streaming text. Context awareness. Minimal chrome. 19,AI/Chatbot Platform,"ai, artificial-intelligence, automation, chatbot, machine-learning, ml, platform",AI-Native UI + Minimalism,"Zero Interface, Glassmorphism",Interactive Product Demo,AI/ML Analytics Dashboard,Neutral + AI Purple (#6366F1),Conversational UI. Streaming text. Context awareness. Minimal chrome.
20,NFT/Web3 Platform,"nft, platform, web",Cyberpunk UI + Glassmorphism,"Aurora UI, 3D & Hyperrealism",Feature-Rich Showcase,Crypto/Blockchain Dashboard,Dark + Neon + Gold (#FFD700),Wallet integration. Transaction feedback. Gas fees display. Dark mode essential. 20,NFT/Web3 Platform,"nft, platform, web",Cyberpunk UI + Glassmorphism,"Aurora UI, 3D & Hyperrealism",Feature-Rich Showcase,Crypto/Blockchain Dashboard,Dark + Neon + Gold (#FFD700),Wallet integration. Transaction feedback. Gas fees display. Dark mode essential.
21,Creator Economy Platform,"creator, economy, platform",Vibrant & Block-based + Bento Box Grid,"Motion-Driven, Aurora UI",Social Proof-Focused,User Behavior Analytics,Vibrant + Brand colors,Creator profiles. Monetization display. Engagement metrics. Social proof. 21,Creator Economy Platform,"creator, economy, platform",Vibrant & Block-based + Bento Box Grid,"Motion-Driven, Aurora UI",Social Proof-Focused,User Behavior Analytics,Vibrant + Brand colors,Creator profiles. Monetization display. Engagement metrics. Social proof.
22,Sustainability/ESG Platform,"ai, artificial-intelligence, automation, esg, machine-learning, ml, platform, sustainability",Organic Biophilic + Minimalism,"Accessible & Ethical, Flat Design",Trust & Authority,Energy/Utilities Dashboard,Green (#228B22) + Earth tones,Carbon footprint visuals. Progress indicators. Certification badges. Eco-friendly imagery. 22,Sustainability/ESG Platform,"ai, artificial-intelligence, automation, esg, machine-learning, ml, platform, sustainability",Organic Biophilic + Minimalism,"Accessible & Ethical, Flat Design",Trust & Authority,Energy/Utilities Dashboard,Green (#228B22) + Earth tones,Carbon footprint visuals. Progress indicators. Certification badges. Eco-friendly imagery.
23,Remote Work/Collaboration Tool,"collaboration, remote, tool, work",Soft UI Evolution + Minimalism,"Glassmorphism, Micro-interactions",Feature-Rich Showcase,Drill-Down Analytics,Calm Blue + Neutral grey,Real-time collaboration. Status indicators. Video integration. Notification management. 23,Remote Work/Collaboration Tool,"collaboration, remote, tool, work",Soft UI Evolution + Minimalism,"Glassmorphism, Micro-interactions",Feature-Rich Showcase,Drill-Down Analytics,Calm Blue + Neutral grey,Real-time collaboration. Status indicators. Video integration. Notification management.
24,Mental Health App,"app, health, mental",Neumorphism + Accessible & Ethical,"Claymorphism, Soft UI Evolution",Social Proof-Focused,Healthcare Analytics,Calm Pastels + Trust colors,Calming aesthetics. Privacy-first. Crisis resources. Progress tracking. Accessibility mandatory. 24,Mental Health App,"app, health, mental",Neumorphism + Accessible & Ethical,"Claymorphism, Soft UI Evolution",Social Proof-Focused,Healthcare Analytics,Calm Pastels + Trust colors,Calming aesthetics. Privacy-first. Crisis resources. Progress tracking. Accessibility mandatory.
25,Pet Tech App,"app, pet, tech",Claymorphism + Vibrant & Block-based,"Micro-interactions, Flat Design",Storytelling-Driven,User Behavior Analytics,Playful + Warm colors,Pet profiles. Health tracking. Playful UI. Photo galleries. Vet integration. 25,Pet Tech App,"app, pet, tech",Claymorphism + Vibrant & Block-based,"Micro-interactions, Flat Design",Storytelling-Driven,User Behavior Analytics,Playful + Warm colors,Pet profiles. Health tracking. Playful UI. Photo galleries. Vet integration.
26,Smart Home/IoT Dashboard,"admin, analytics, dashboard, data, home, iot, panel, smart",Glassmorphism + Dark Mode (OLED),"Minimalism, AI-Native UI",Interactive Product Demo,Real-Time Monitoring,Dark + Status indicator colors,Device status. Real-time controls. Energy monitoring. Automation rules. Quick actions. 26,Smart Home/IoT Dashboard,"admin, analytics, dashboard, data, home, iot, panel, smart",Glassmorphism + Dark Mode (OLED),"Minimalism, AI-Native UI",Interactive Product Demo,Real-Time Monitoring,Dark + Status indicator colors,Device status. Real-time controls. Energy monitoring. Automation rules. Quick actions.
27,EV/Charging Ecosystem,"charging, ecosystem, ev",Minimalism + Aurora UI,"Glassmorphism, Organic Biophilic",Hero-Centric Design,Energy/Utilities Dashboard,Electric Blue (#009CD1) + Green,Charging station maps. Range estimation. Cost calculation. Environmental impact. 27,EV/Charging Ecosystem,"charging, ecosystem, ev",Minimalism + Aurora UI,"Glassmorphism, Organic Biophilic",Hero-Centric Design,Energy/Utilities Dashboard,Electric Blue (#009CD1) + Green,Charging station maps. Range estimation. Cost calculation. Environmental impact.
28,Subscription Box Service,"appointment, booking, box, consultation, membership, plan, recurring, service, subscription",Vibrant & Block-based + Motion-Driven,"Claymorphism, Aurora UI",Feature-Rich Showcase,E-commerce Analytics,Brand + Excitement colors,Unboxing experience. Personalization quiz. Subscription management. Product reveals. 28,Subscription Box Service,"appointment, booking, box, consultation, membership, plan, recurring, service, subscription",Vibrant & Block-based + Motion-Driven,"Claymorphism, Aurora UI",Feature-Rich Showcase,E-commerce Analytics,Brand + Excitement colors,Unboxing experience. Personalization quiz. Subscription management. Product reveals.
29,Podcast Platform,"platform, podcast",Dark Mode (OLED) + Minimalism,"Motion-Driven, Vibrant & Block-based",Storytelling-Driven,Media/Entertainment Dashboard,Dark + Audio waveform accents,Audio player UX. Episode discovery. Creator tools. Analytics for podcasters. 29,Podcast Platform,"platform, podcast",Dark Mode (OLED) + Minimalism,"Motion-Driven, Vibrant & Block-based",Storytelling-Driven,Media/Entertainment Dashboard,Dark + Audio waveform accents,Audio player UX. Episode discovery. Creator tools. Analytics for podcasters.
30,Dating App,"app, dating",Vibrant & Block-based + Motion-Driven,"Aurora UI, Glassmorphism",Social Proof-Focused,User Behavior Analytics,Warm + Romantic (Pink/Red gradients),Profile cards. Swipe interactions. Match animations. Safety features. Video chat. 30,Dating App,"app, dating",Vibrant & Block-based + Motion-Driven,"Aurora UI, Glassmorphism",Social Proof-Focused,User Behavior Analytics,Warm + Romantic (Pink/Red gradients),Profile cards. Swipe interactions. Match animations. Safety features. Video chat.
31,Micro-Credentials/Badges Platform,"badges, credentials, micro, platform",Minimalism + Flat Design,"Accessible & Ethical, Swiss Modernism 2.0",Trust & Authority,Education Dashboard,Trust Blue + Gold (#FFD700),Credential verification. Badge display. Progress tracking. Issuer trust. LinkedIn integration. 31,Micro-Credentials/Badges Platform,"badges, credentials, micro, platform",Minimalism + Flat Design,"Accessible & Ethical, Swiss Modernism 2.0",Trust & Authority,Education Dashboard,Trust Blue + Gold (#FFD700),Credential verification. Badge display. Progress tracking. Issuer trust. LinkedIn integration.
32,Knowledge Base/Documentation,"base, documentation, knowledge",Minimalism + Accessible & Ethical,"Swiss Modernism 2.0, Flat Design",FAQ/Documentation,N/A - Documentation focused,Clean hierarchy + minimal color,Search-first. Clear navigation. Code highlighting. Version switching. Feedback system. 32,Knowledge Base/Documentation,"base, documentation, knowledge",Minimalism + Accessible & Ethical,"Swiss Modernism 2.0, Flat Design",FAQ/Documentation,N/A - Documentation focused,Clean hierarchy + minimal color,Search-first. Clear navigation. Code highlighting. Version switching. Feedback system.
33,Hyperlocal Services,"appointment, booking, consultation, hyperlocal, service, services",Minimalism + Vibrant & Block-based,"Micro-interactions, Flat Design",Conversion-Optimized,Drill-Down Analytics + Map,Location markers + Trust colors,Map integration. Service categories. Provider profiles. Booking system. Reviews. 33,Hyperlocal Services,"appointment, booking, consultation, hyperlocal, service, services",Minimalism + Vibrant & Block-based,"Micro-interactions, Flat Design",Conversion-Optimized,Drill-Down Analytics + Map,Location markers + Trust colors,Map integration. Service categories. Provider profiles. Booking system. Reviews.
34,Beauty/Spa/Wellness Service,"appointment, beauty, booking, consultation, service, spa, wellness",Soft UI Evolution + Neumorphism,"Glassmorphism, Minimalism",Hero-Centric Design + Social Proof,User Behavior Analytics,Soft pastels (Pink #FFB6C1 Sage #90EE90) + Cream + Gold accents,Calming aesthetic. Booking system. Service menu. Before/after gallery. Testimonials. Relaxing imagery. 34,Beauty/Spa/Wellness Service,"appointment, beauty, booking, consultation, service, spa, wellness",Soft UI Evolution + Neumorphism,"Glassmorphism, Minimalism",Hero-Centric Design + Social Proof,User Behavior Analytics,Soft pastels (Pink #FFB6C1 Sage #90EE90) + Cream + Gold accents,Calming aesthetic. Booking system. Service menu. Before/after gallery. Testimonials. Relaxing imagery.
35,Luxury/Premium Brand,"brand, elegant, exclusive, high-end, luxury, premium",Liquid Glass + Glassmorphism,"Minimalism, 3D & Hyperrealism",Storytelling-Driven + Feature-Rich,Sales Intelligence Dashboard,Black + Gold (#FFD700) + White + Minimal accent,Elegance paramount. Premium imagery. Storytelling. High-quality visuals. Exclusive feel. 35,Luxury/Premium Brand,"brand, elegant, exclusive, high-end, luxury, premium",Liquid Glass + Glassmorphism,"Minimalism, 3D & Hyperrealism",Storytelling-Driven + Feature-Rich,Sales Intelligence Dashboard,Black + Gold (#FFD700) + White + Minimal accent,Elegance paramount. Premium imagery. Storytelling. High-quality visuals. Exclusive feel.
36,Restaurant/Food Service,"appointment, booking, consultation, delivery, food, menu, order, restaurant, service",Vibrant & Block-based + Motion-Driven,"Claymorphism, Flat Design",Hero-Centric Design + Conversion,N/A - Booking focused,Warm colors (Orange Red Brown) + appetizing imagery,Menu display. Online ordering. Reservation system. Food photography. Location/hours prominent. 36,Restaurant/Food Service,"appointment, booking, consultation, delivery, food, menu, order, restaurant, service",Vibrant & Block-based + Motion-Driven,"Claymorphism, Flat Design",Hero-Centric Design + Conversion,N/A - Booking focused,Warm colors (Orange Red Brown) + appetizing imagery,Menu display. Online ordering. Reservation system. Food photography. Location/hours prominent.
37,Fitness/Gym App,"app, exercise, fitness, gym, health, workout",Vibrant & Block-based + Dark Mode (OLED),"Motion-Driven, Neumorphism",Feature-Rich Showcase,User Behavior Analytics,Energetic (Orange #FF6B35 Electric Blue) + Dark bg,Progress tracking. Workout plans. Community features. Achievements. Motivational design. 37,Fitness/Gym App,"app, exercise, fitness, gym, health, workout",Vibrant & Block-based + Dark Mode (OLED),"Motion-Driven, Neumorphism",Feature-Rich Showcase,User Behavior Analytics,Energetic (Orange #FF6B35 Electric Blue) + Dark bg,Progress tracking. Workout plans. Community features. Achievements. Motivational design.
38,Real Estate/Property,"buy, estate, housing, property, real, real-estate, rent",Glassmorphism + Minimalism,"Motion-Driven, 3D & Hyperrealism",Hero-Centric Design + Feature-Rich,Sales Intelligence Dashboard,Trust Blue (#0077B6) + Gold accents + White,Property listings. Virtual tours. Map integration. Agent profiles. Mortgage calculator. High-quality imagery. 38,Real Estate/Property,"buy, estate, housing, property, real, real-estate, rent",Glassmorphism + Minimalism,"Motion-Driven, 3D & Hyperrealism",Hero-Centric Design + Feature-Rich,Sales Intelligence Dashboard,Trust Blue (#0077B6) + Gold accents + White,Property listings. Virtual tours. Map integration. Agent profiles. Mortgage calculator. High-quality imagery.
39,Travel/Tourism Agency,"agency, booking, creative, design, flight, hotel, marketing, studio, tourism, travel, vacation",Aurora UI + Motion-Driven,"Vibrant & Block-based, Glassmorphism",Storytelling-Driven + Hero-Centric,Booking Analytics,Vibrant destination colors + Sky Blue + Warm accents,Destination showcase. Booking system. Itinerary builder. Reviews. Inspiration galleries. Mobile-first. 39,Travel/Tourism Agency,"agency, booking, creative, design, flight, hotel, marketing, studio, tourism, travel, vacation",Aurora UI + Motion-Driven,"Vibrant & Block-based, Glassmorphism",Storytelling-Driven + Hero-Centric,Booking Analytics,Vibrant destination colors + Sky Blue + Warm accents,Destination showcase. Booking system. Itinerary builder. Reviews. Inspiration galleries. Mobile-first.
40,Hotel/Hospitality,"hospitality, hotel",Liquid Glass + Minimalism,"Glassmorphism, Soft UI Evolution",Hero-Centric Design + Social Proof,Revenue Management Dashboard,Warm neutrals + Gold (#D4AF37) + Brand accent,Room booking. Amenities showcase. Location maps. Guest reviews. Seasonal pricing. Luxury imagery. 40,Hotel/Hospitality,"hospitality, hotel",Liquid Glass + Minimalism,"Glassmorphism, Soft UI Evolution",Hero-Centric Design + Social Proof,Revenue Management Dashboard,Warm neutrals + Gold (#D4AF37) + Brand accent,Room booking. Amenities showcase. Location maps. Guest reviews. Seasonal pricing. Luxury imagery.
41,Wedding/Event Planning,"conference, event, meetup, planning, registration, ticket, wedding",Soft UI Evolution + Aurora UI,"Glassmorphism, Motion-Driven",Storytelling-Driven + Social Proof,N/A - Planning focused,Soft Pink (#FFD6E0) + Gold + Cream + Sage,Portfolio gallery. Vendor directory. Planning tools. Timeline. Budget tracker. Romantic aesthetic. 41,Wedding/Event Planning,"conference, event, meetup, planning, registration, ticket, wedding",Soft UI Evolution + Aurora UI,"Glassmorphism, Motion-Driven",Storytelling-Driven + Social Proof,N/A - Planning focused,Soft Pink (#FFD6E0) + Gold + Cream + Sage,Portfolio gallery. Vendor directory. Planning tools. Timeline. Budget tracker. Romantic aesthetic.
42,Legal Services,"appointment, attorney, booking, compliance, consultation, contract, law, legal, service, services",Trust & Authority + Minimalism,"Accessible & Ethical, Swiss Modernism 2.0",Trust & Authority + Minimal,Case Management Dashboard,Navy Blue (#1E3A5F) + Gold + White,Credibility paramount. Practice areas. Attorney profiles. Case results. Contact forms. Professional imagery. 42,Legal Services,"appointment, attorney, booking, compliance, consultation, contract, law, legal, service, services",Trust & Authority + Minimalism,"Accessible & Ethical, Swiss Modernism 2.0",Trust & Authority + Minimal,Case Management Dashboard,Navy Blue (#1E3A5F) + Gold + White,Credibility paramount. Practice areas. Attorney profiles. Case results. Contact forms. Professional imagery.
43,Insurance Platform,"insurance, platform",Trust & Authority + Flat Design,"Accessible & Ethical, Minimalism",Conversion-Optimized + Trust,Claims Analytics Dashboard,Trust Blue (#0066CC) + Green (security) + Neutral,Quote calculator. Policy comparison. Claims process. Trust signals. Clear pricing. Security badges. 43,Insurance Platform,"insurance, platform",Trust & Authority + Flat Design,"Accessible & Ethical, Minimalism",Conversion-Optimized + Trust,Claims Analytics Dashboard,Trust Blue (#0066CC) + Green (security) + Neutral,Quote calculator. Policy comparison. Claims process. Trust signals. Clear pricing. Security badges.
44,Banking/Traditional Finance,"banking, finance, traditional",Minimalism + Accessible & Ethical,"Trust & Authority, Dark Mode (OLED)",Trust & Authority + Feature-Rich,Financial Dashboard,Navy (#0A1628) + Trust Blue + Gold accents,Security-first. Account overview. Transaction history. Mobile banking. Accessibility critical. Trust paramount. 44,Banking/Traditional Finance,"banking, finance, traditional",Minimalism + Accessible & Ethical,"Trust & Authority, Dark Mode (OLED)",Trust & Authority + Feature-Rich,Financial Dashboard,Navy (#0A1628) + Trust Blue + Gold accents,Security-first. Account overview. Transaction history. Mobile banking. Accessibility critical. Trust paramount.
45,Online Course/E-learning,"course, e, learning, online",Claymorphism + Vibrant & Block-based,"Motion-Driven, Flat Design",Feature-Rich Showcase + Social Proof,Education Dashboard,Vibrant learning colors + Progress green,Course catalog. Progress tracking. Video player. Quizzes. Certificates. Community forums. Gamification. 45,Online Course/E-learning,"course, e, learning, online",Claymorphism + Vibrant & Block-based,"Motion-Driven, Flat Design",Feature-Rich Showcase + Social Proof,Education Dashboard,Vibrant learning colors + Progress green,Course catalog. Progress tracking. Video player. Quizzes. Certificates. Community forums. Gamification.
46,Non-profit/Charity,"charity, non, profit",Accessible & Ethical + Organic Biophilic,"Minimalism, Storytelling-Driven",Storytelling-Driven + Trust,Donation Analytics Dashboard,Cause-related colors + Trust + Warm,Impact stories. Donation flow. Transparency reports. Volunteer signup. Event calendar. Emotional connection. 46,Non-profit/Charity,"charity, non, profit",Accessible & Ethical + Organic Biophilic,"Minimalism, Storytelling-Driven",Storytelling-Driven + Trust,Donation Analytics Dashboard,Cause-related colors + Trust + Warm,Impact stories. Donation flow. Transparency reports. Volunteer signup. Event calendar. Emotional connection.
47,Music Streaming,"music, streaming",Dark Mode (OLED) + Vibrant & Block-based,"Motion-Driven, Aurora UI",Feature-Rich Showcase,Media/Entertainment Dashboard,Dark (#121212) + Vibrant accents + Album art colors,Audio player. Playlist management. Artist pages. Personalization. Social features. Waveform visualizations. 47,Music Streaming,"music, streaming",Dark Mode (OLED) + Vibrant & Block-based,"Motion-Driven, Aurora UI",Feature-Rich Showcase,Media/Entertainment Dashboard,Dark (#121212) + Vibrant accents + Album art colors,Audio player. Playlist management. Artist pages. Personalization. Social features. Waveform visualizations.
48,Video Streaming/OTT,"ott, streaming, video",Dark Mode (OLED) + Motion-Driven,"Glassmorphism, Vibrant & Block-based",Hero-Centric Design + Feature-Rich,Media/Entertainment Dashboard,Dark bg + Content poster colors + Brand accent,Video player. Content discovery. Watchlist. Continue watching. Personalized recommendations. Thumbnail-heavy. 48,Video Streaming/OTT,"ott, streaming, video",Dark Mode (OLED) + Motion-Driven,"Glassmorphism, Vibrant & Block-based",Hero-Centric Design + Feature-Rich,Media/Entertainment Dashboard,Dark bg + Content poster colors + Brand accent,Video player. Content discovery. Watchlist. Continue watching. Personalized recommendations. Thumbnail-heavy.
49,Job Board/Recruitment,"board, job, recruitment",Flat Design + Minimalism,"Vibrant & Block-based, Accessible & Ethical",Conversion-Optimized + Feature-Rich,HR Analytics Dashboard,Professional Blue + Success Green + Neutral,Job listings. Search/filter. Company profiles. Application tracking. Resume upload. Salary insights. 49,Job Board/Recruitment,"board, job, recruitment",Flat Design + Minimalism,"Vibrant & Block-based, Accessible & Ethical",Conversion-Optimized + Feature-Rich,HR Analytics Dashboard,Professional Blue + Success Green + Neutral,Job listings. Search/filter. Company profiles. Application tracking. Resume upload. Salary insights.
50,Marketplace (P2P),"buyers, listings, marketplace, p, platform, sellers",Vibrant & Block-based + Flat Design,"Micro-interactions, Trust & Authority",Feature-Rich Showcase + Social Proof,E-commerce Analytics,Trust colors + Category colors + Success green,Seller/buyer profiles. Listings. Reviews/ratings. Secure payment. Messaging. Search/filter. Trust badges. 50,Marketplace (P2P),"buyers, listings, marketplace, p, platform, sellers",Vibrant & Block-based + Flat Design,"Micro-interactions, Trust & Authority",Feature-Rich Showcase + Social Proof,E-commerce Analytics,Trust colors + Category colors + Success green,Seller/buyer profiles. Listings. Reviews/ratings. Secure payment. Messaging. Search/filter. Trust badges.
51,Logistics/Delivery,"delivery, logistics",Minimalism + Flat Design,"Dark Mode (OLED), Micro-interactions",Feature-Rich Showcase + Conversion,Real-Time Monitoring + Route Analytics,Blue (#2563EB) + Orange (tracking) + Green (delivered),Real-time tracking. Delivery scheduling. Route optimization. Driver management. Status updates. Map integration. 51,Logistics/Delivery,"delivery, logistics",Minimalism + Flat Design,"Dark Mode (OLED), Micro-interactions",Feature-Rich Showcase + Conversion,Real-Time Monitoring + Route Analytics,Blue (#2563EB) + Orange (tracking) + Green (delivered),Real-time tracking. Delivery scheduling. Route optimization. Driver management. Status updates. Map integration.
52,Agriculture/Farm Tech,"agriculture, farm, tech",Organic Biophilic + Flat Design,"Minimalism, Accessible & Ethical",Feature-Rich Showcase + Trust,IoT Sensor Dashboard,Earth Green (#4A7C23) + Brown + Sky Blue,Crop monitoring. Weather data. IoT sensors. Yield tracking. Market prices. Sustainable imagery. 52,Agriculture/Farm Tech,"agriculture, farm, tech",Organic Biophilic + Flat Design,"Minimalism, Accessible & Ethical",Feature-Rich Showcase + Trust,IoT Sensor Dashboard,Earth Green (#4A7C23) + Brown + Sky Blue,Crop monitoring. Weather data. IoT sensors. Yield tracking. Market prices. Sustainable imagery.
53,Construction/Architecture,"architecture, construction",Minimalism + 3D & Hyperrealism,"Brutalism, Swiss Modernism 2.0",Hero-Centric Design + Feature-Rich,Project Management Dashboard,Grey (#4A4A4A) + Orange (safety) + Blueprint Blue,Project portfolio. 3D renders. Timeline. Material specs. Team collaboration. Blueprint aesthetic. 53,Construction/Architecture,"architecture, construction",Minimalism + 3D & Hyperrealism,"Brutalism, Swiss Modernism 2.0",Hero-Centric Design + Feature-Rich,Project Management Dashboard,Grey (#4A4A4A) + Orange (safety) + Blueprint Blue,Project portfolio. 3D renders. Timeline. Material specs. Team collaboration. Blueprint aesthetic.
54,Automotive/Car Dealership,"automotive, car, dealership",Motion-Driven + 3D & Hyperrealism,"Dark Mode (OLED), Glassmorphism",Hero-Centric Design + Feature-Rich,Sales Intelligence Dashboard,Brand colors + Metallic accents + Dark/Light,Vehicle showcase. 360° views. Comparison tools. Financing calculator. Test drive booking. High-quality imagery. 54,Automotive/Car Dealership,"automotive, car, dealership",Motion-Driven + 3D & Hyperrealism,"Dark Mode (OLED), Glassmorphism",Hero-Centric Design + Feature-Rich,Sales Intelligence Dashboard,Brand colors + Metallic accents + Dark/Light,Vehicle showcase. 360° views. Comparison tools. Financing calculator. Test drive booking. High-quality imagery.
55,Photography Studio,"photography, studio",Motion-Driven + Minimalism,"Aurora UI, Glassmorphism",Storytelling-Driven + Hero-Centric,N/A - Portfolio focused,Black + White + Minimal accent,Portfolio gallery. Before/after. Service packages. Booking system. Client galleries. Full-bleed imagery. 55,Photography Studio,"photography, studio",Motion-Driven + Minimalism,"Aurora UI, Glassmorphism",Storytelling-Driven + Hero-Centric,N/A - Portfolio focused,Black + White + Minimal accent,Portfolio gallery. Before/after. Service packages. Booking system. Client galleries. Full-bleed imagery.
56,Coworking Space,"coworking, space",Vibrant & Block-based + Glassmorphism,"Minimalism, Motion-Driven",Hero-Centric Design + Feature-Rich,Occupancy Dashboard,Energetic colors + Wood tones + Brand accent,Space tour. Membership plans. Booking system. Amenities. Community events. Virtual tour. 56,Coworking Space,"coworking, space",Vibrant & Block-based + Glassmorphism,"Minimalism, Motion-Driven",Hero-Centric Design + Feature-Rich,Occupancy Dashboard,Energetic colors + Wood tones + Brand accent,Space tour. Membership plans. Booking system. Amenities. Community events. Virtual tour.
57,Cleaning Service,"appointment, booking, cleaning, consultation, service",Soft UI Evolution + Flat Design,"Minimalism, Micro-interactions",Conversion-Optimized + Trust,Service Analytics,Fresh Blue (#00B4D8) + Clean White + Green,Service packages. Booking system. Price calculator. Before/after gallery. Reviews. Trust badges. 57,Cleaning Service,"appointment, booking, cleaning, consultation, service",Soft UI Evolution + Flat Design,"Minimalism, Micro-interactions",Conversion-Optimized + Trust,Service Analytics,Fresh Blue (#00B4D8) + Clean White + Green,Service packages. Booking system. Price calculator. Before/after gallery. Reviews. Trust badges.
58,Home Services (Plumber/Electrician),"appointment, booking, consultation, electrician, home, plumber, service, services",Flat Design + Trust & Authority,"Minimalism, Accessible & Ethical",Conversion-Optimized + Trust,Service Analytics,Trust Blue + Safety Orange + Professional grey,Service list. Emergency contact. Booking. Price transparency. Certifications. Local trust signals. 58,Home Services (Plumber/Electrician),"appointment, booking, consultation, electrician, home, plumber, service, services",Flat Design + Trust & Authority,"Minimalism, Accessible & Ethical",Conversion-Optimized + Trust,Service Analytics,Trust Blue + Safety Orange + Professional grey,Service list. Emergency contact. Booking. Price transparency. Certifications. Local trust signals.
59,Childcare/Daycare,"childcare, daycare",Claymorphism + Vibrant & Block-based,"Soft UI Evolution, Accessible & Ethical",Social Proof-Focused + Trust,Parent Dashboard,Playful pastels + Safe colors + Warm accents,Programs. Staff profiles. Safety certifications. Parent portal. Activity updates. Cheerful imagery. 59,Childcare/Daycare,"childcare, daycare",Claymorphism + Vibrant & Block-based,"Soft UI Evolution, Accessible & Ethical",Social Proof-Focused + Trust,Parent Dashboard,Playful pastels + Safe colors + Warm accents,Programs. Staff profiles. Safety certifications. Parent portal. Activity updates. Cheerful imagery.
60,Senior Care/Elderly,"care, elderly, senior",Accessible & Ethical + Soft UI Evolution,"Minimalism, Neumorphism",Trust & Authority + Social Proof,Healthcare Analytics,Calm Blue + Warm neutrals + Large text,Care services. Staff qualifications. Facility tour. Family portal. Large touch targets. High contrast. Accessibility-first. 60,Senior Care/Elderly,"care, elderly, senior",Accessible & Ethical + Soft UI Evolution,"Minimalism, Neumorphism",Trust & Authority + Social Proof,Healthcare Analytics,Calm Blue + Warm neutrals + Large text,Care services. Staff qualifications. Facility tour. Family portal. Large touch targets. High contrast. Accessibility-first.
61,Medical Clinic,"clinic, medical",Accessible & Ethical + Minimalism,"Neumorphism, Trust & Authority",Trust & Authority + Conversion,Healthcare Analytics,Medical Blue (#0077B6) + Trust White + Calm Green,Services. Doctor profiles. Online booking. Patient portal. Insurance info. HIPAA compliant. Trust signals. 61,Medical Clinic,"clinic, medical",Accessible & Ethical + Minimalism,"Neumorphism, Trust & Authority",Trust & Authority + Conversion,Healthcare Analytics,Medical Blue (#0077B6) + Trust White + Calm Green,Services. Doctor profiles. Online booking. Patient portal. Insurance info. HIPAA compliant. Trust signals.
62,Pharmacy/Drug Store,"drug, pharmacy, store",Flat Design + Accessible & Ethical,"Minimalism, Trust & Authority",Conversion-Optimized + Trust,Inventory Dashboard,Pharmacy Green + Trust Blue + Clean White,Product catalog. Prescription upload. Refill reminders. Health info. Store locator. Safety certifications. 62,Pharmacy/Drug Store,"drug, pharmacy, store",Flat Design + Accessible & Ethical,"Minimalism, Trust & Authority",Conversion-Optimized + Trust,Inventory Dashboard,Pharmacy Green + Trust Blue + Clean White,Product catalog. Prescription upload. Refill reminders. Health info. Store locator. Safety certifications.
63,Dental Practice,"dental, practice",Soft UI Evolution + Minimalism,"Accessible & Ethical, Trust & Authority",Social Proof-Focused + Conversion,Patient Analytics,Fresh Blue + White + Smile Yellow accent,Services. Dentist profiles. Before/after. Online booking. Insurance. Patient testimonials. Friendly imagery. 63,Dental Practice,"dental, practice",Soft UI Evolution + Minimalism,"Accessible & Ethical, Trust & Authority",Social Proof-Focused + Conversion,Patient Analytics,Fresh Blue + White + Smile Yellow accent,Services. Dentist profiles. Before/after. Online booking. Insurance. Patient testimonials. Friendly imagery.
64,Veterinary Clinic,"clinic, veterinary",Claymorphism + Accessible & Ethical,"Soft UI Evolution, Flat Design",Social Proof-Focused + Trust,Pet Health Dashboard,Caring Blue + Pet-friendly colors + Warm accents,Pet services. Vet profiles. Online booking. Pet portal. Emergency info. Friendly animal imagery. 64,Veterinary Clinic,"clinic, veterinary",Claymorphism + Accessible & Ethical,"Soft UI Evolution, Flat Design",Social Proof-Focused + Trust,Pet Health Dashboard,Caring Blue + Pet-friendly colors + Warm accents,Pet services. Vet profiles. Online booking. Pet portal. Emergency info. Friendly animal imagery.
65,Florist/Plant Shop,"florist, plant, shop",Organic Biophilic + Vibrant & Block-based,"Aurora UI, Motion-Driven",Hero-Centric Design + Conversion,E-commerce Analytics,Natural Green + Floral pinks/purples + Earth tones,Product catalog. Occasion categories. Delivery scheduling. Care guides. Seasonal collections. Beautiful imagery. 65,Florist/Plant Shop,"florist, plant, shop",Organic Biophilic + Vibrant & Block-based,"Aurora UI, Motion-Driven",Hero-Centric Design + Conversion,E-commerce Analytics,Natural Green + Floral pinks/purples + Earth tones,Product catalog. Occasion categories. Delivery scheduling. Care guides. Seasonal collections. Beautiful imagery.
66,Bakery/Cafe,"bakery, cafe",Vibrant & Block-based + Soft UI Evolution,"Claymorphism, Motion-Driven",Hero-Centric Design + Conversion,N/A - Order focused,Warm Brown + Cream + Appetizing accents,Menu display. Online ordering. Location/hours. Catering. Seasonal specials. Appetizing photography. 66,Bakery/Cafe,"bakery, cafe",Vibrant & Block-based + Soft UI Evolution,"Claymorphism, Motion-Driven",Hero-Centric Design + Conversion,N/A - Order focused,Warm Brown + Cream + Appetizing accents,Menu display. Online ordering. Location/hours. Catering. Seasonal specials. Appetizing photography.
67,Coffee Shop,"coffee, shop",Minimalism + Organic Biophilic,"Soft UI Evolution, Flat Design",Hero-Centric Design + Conversion,N/A - Order focused,Coffee Brown (#6F4E37) + Cream + Warm accents,Menu. Online ordering. Loyalty program. Location. Story/origin. Cozy aesthetic. 67,Coffee Shop,"coffee, shop",Minimalism + Organic Biophilic,"Soft UI Evolution, Flat Design",Hero-Centric Design + Conversion,N/A - Order focused,Coffee Brown (#6F4E37) + Cream + Warm accents,Menu. Online ordering. Loyalty program. Location. Story/origin. Cozy aesthetic.
68,Brewery/Winery,"brewery, winery",Motion-Driven + Storytelling-Driven,"Dark Mode (OLED), Organic Biophilic",Storytelling-Driven + Hero-Centric,N/A - E-commerce focused,Deep amber/burgundy + Gold + Craft aesthetic,Product showcase. Story/heritage. Tasting notes. Events. Club membership. Artisanal imagery. 68,Brewery/Winery,"brewery, winery",Motion-Driven + Storytelling-Driven,"Dark Mode (OLED), Organic Biophilic",Storytelling-Driven + Hero-Centric,N/A - E-commerce focused,Deep amber/burgundy + Gold + Craft aesthetic,Product showcase. Story/heritage. Tasting notes. Events. Club membership. Artisanal imagery.
69,Airline,"ai, airline, artificial-intelligence, automation, machine-learning, ml",Minimalism + Glassmorphism,"Motion-Driven, Accessible & Ethical",Conversion-Optimized + Feature-Rich,Operations Dashboard,Sky Blue + Brand colors + Trust accents,Flight search. Booking. Check-in. Boarding pass. Loyalty program. Route maps. Mobile-first. 69,Airline,"ai, airline, artificial-intelligence, automation, machine-learning, ml",Minimalism + Glassmorphism,"Motion-Driven, Accessible & Ethical",Conversion-Optimized + Feature-Rich,Operations Dashboard,Sky Blue + Brand colors + Trust accents,Flight search. Booking. Check-in. Boarding pass. Loyalty program. Route maps. Mobile-first.
70,News/Media Platform,"content, entertainment, media, news, platform, streaming, video",Minimalism + Flat Design,"Dark Mode (OLED), Accessible & Ethical",Hero-Centric Design + Feature-Rich,Media Analytics Dashboard,Brand colors + High contrast + Category colors,Article layout. Breaking news. Categories. Search. Subscription. Mobile reading. Fast loading. 70,News/Media Platform,"content, entertainment, media, news, platform, streaming, video",Minimalism + Flat Design,"Dark Mode (OLED), Accessible & Ethical",Hero-Centric Design + Feature-Rich,Media Analytics Dashboard,Brand colors + High contrast + Category colors,Article layout. Breaking news. Categories. Search. Subscription. Mobile reading. Fast loading.
71,Magazine/Blog,"articles, blog, content, magazine, posts, writing",Swiss Modernism 2.0 + Motion-Driven,"Minimalism, Aurora UI",Storytelling-Driven + Hero-Centric,Content Analytics,Editorial colors + Brand primary + Clean white,Article showcase. Category navigation. Author profiles. Newsletter signup. Related content. Typography-focused. 71,Magazine/Blog,"articles, blog, content, magazine, posts, writing",Swiss Modernism 2.0 + Motion-Driven,"Minimalism, Aurora UI",Storytelling-Driven + Hero-Centric,Content Analytics,Editorial colors + Brand primary + Clean white,Article showcase. Category navigation. Author profiles. Newsletter signup. Related content. Typography-focused.
72,Freelancer Platform,"freelancer, platform",Flat Design + Minimalism,"Vibrant & Block-based, Micro-interactions",Feature-Rich Showcase + Conversion,Marketplace Analytics,Professional Blue + Success Green + Neutral,Profile creation. Portfolio. Skill matching. Messaging. Payment. Reviews. Project management. 72,Freelancer Platform,"freelancer, platform",Flat Design + Minimalism,"Vibrant & Block-based, Micro-interactions",Feature-Rich Showcase + Conversion,Marketplace Analytics,Professional Blue + Success Green + Neutral,Profile creation. Portfolio. Skill matching. Messaging. Payment. Reviews. Project management.
73,Consulting Firm,"consulting, firm",Trust & Authority + Minimalism,"Swiss Modernism 2.0, Accessible & Ethical",Trust & Authority + Feature-Rich,N/A - Lead generation,Navy + Gold + Professional grey,Service areas. Case studies. Team profiles. Thought leadership. Contact. Professional credibility. 73,Consulting Firm,"consulting, firm",Trust & Authority + Minimalism,"Swiss Modernism 2.0, Accessible & Ethical",Trust & Authority + Feature-Rich,N/A - Lead generation,Navy + Gold + Professional grey,Service areas. Case studies. Team profiles. Thought leadership. Contact. Professional credibility.
74,Marketing Agency,"agency, creative, design, marketing, studio",Brutalism + Motion-Driven,"Vibrant & Block-based, Aurora UI",Storytelling-Driven + Feature-Rich,Campaign Analytics,Bold brand colors + Creative freedom,Portfolio. Case studies. Services. Team. Creative showcase. Results-focused. Bold aesthetic. 74,Marketing Agency,"agency, creative, design, marketing, studio",Brutalism + Motion-Driven,"Vibrant & Block-based, Aurora UI",Storytelling-Driven + Feature-Rich,Campaign Analytics,Bold brand colors + Creative freedom,Portfolio. Case studies. Services. Team. Creative showcase. Results-focused. Bold aesthetic.
75,Event Management,"conference, event, management, meetup, registration, ticket",Vibrant & Block-based + Motion-Driven,"Glassmorphism, Aurora UI",Hero-Centric Design + Feature-Rich,Event Analytics,Event theme colors + Excitement accents,Event showcase. Registration. Agenda. Speakers. Sponsors. Ticket sales. Countdown timer. 75,Event Management,"conference, event, management, meetup, registration, ticket",Vibrant & Block-based + Motion-Driven,"Glassmorphism, Aurora UI",Hero-Centric Design + Feature-Rich,Event Analytics,Event theme colors + Excitement accents,Event showcase. Registration. Agenda. Speakers. Sponsors. Ticket sales. Countdown timer.
76,Conference/Webinar Platform,"conference, platform, webinar",Glassmorphism + Minimalism,"Motion-Driven, Flat Design",Feature-Rich Showcase + Conversion,Attendee Analytics,Professional Blue + Video accent + Brand,Registration. Agenda. Speaker profiles. Live stream. Networking. Recording access. Virtual event features. 76,Conference/Webinar Platform,"conference, platform, webinar",Glassmorphism + Minimalism,"Motion-Driven, Flat Design",Feature-Rich Showcase + Conversion,Attendee Analytics,Professional Blue + Video accent + Brand,Registration. Agenda. Speaker profiles. Live stream. Networking. Recording access. Virtual event features.
77,Membership/Community,"community, membership",Vibrant & Block-based + Soft UI Evolution,"Bento Box Grid, Micro-interactions",Social Proof-Focused + Conversion,Community Analytics,Community brand colors + Engagement accents,Member benefits. Pricing tiers. Community showcase. Events. Member directory. Exclusive content. 77,Membership/Community,"community, membership",Vibrant & Block-based + Soft UI Evolution,"Bento Box Grid, Micro-interactions",Social Proof-Focused + Conversion,Community Analytics,Community brand colors + Engagement accents,Member benefits. Pricing tiers. Community showcase. Events. Member directory. Exclusive content.
78,Newsletter Platform,"newsletter, platform",Minimalism + Flat Design,"Swiss Modernism 2.0, Accessible & Ethical",Minimal & Direct + Conversion,Email Analytics,Brand primary + Clean white + CTA accent,Subscribe form. Archive. About. Social proof. Sample content. Simple conversion. 78,Newsletter Platform,"newsletter, platform",Minimalism + Flat Design,"Swiss Modernism 2.0, Accessible & Ethical",Minimal & Direct + Conversion,Email Analytics,Brand primary + Clean white + CTA accent,Subscribe form. Archive. About. Social proof. Sample content. Simple conversion.
79,Digital Products/Downloads,"digital, downloads, products",Vibrant & Block-based + Motion-Driven,"Glassmorphism, Bento Box Grid",Feature-Rich Showcase + Conversion,E-commerce Analytics,Product category colors + Brand + Success green,Product showcase. Preview. Pricing. Instant delivery. License management. Customer reviews. 79,Digital Products/Downloads,"digital, downloads, products",Vibrant & Block-based + Motion-Driven,"Glassmorphism, Bento Box Grid",Feature-Rich Showcase + Conversion,E-commerce Analytics,Product category colors + Brand + Success green,Product showcase. Preview. Pricing. Instant delivery. License management. Customer reviews.
80,Church/Religious Organization,"church, organization, religious",Accessible & Ethical + Soft UI Evolution,"Minimalism, Trust & Authority",Hero-Centric Design + Social Proof,N/A - Community focused,Warm Gold + Deep Purple/Blue + White,Service times. Events. Sermons. Community. Giving. Location. Welcoming imagery. 80,Church/Religious Organization,"church, organization, religious",Accessible & Ethical + Soft UI Evolution,"Minimalism, Trust & Authority",Hero-Centric Design + Social Proof,N/A - Community focused,Warm Gold + Deep Purple/Blue + White,Service times. Events. Sermons. Community. Giving. Location. Welcoming imagery.
81,Sports Team/Club,"club, sports, team",Vibrant & Block-based + Motion-Driven,"Dark Mode (OLED), 3D & Hyperrealism",Hero-Centric Design + Feature-Rich,Performance Analytics,Team colors + Energetic accents,Schedule. Roster. News. Tickets. Merchandise. Fan engagement. Action imagery. 81,Sports Team/Club,"club, sports, team",Vibrant & Block-based + Motion-Driven,"Dark Mode (OLED), 3D & Hyperrealism",Hero-Centric Design + Feature-Rich,Performance Analytics,Team colors + Energetic accents,Schedule. Roster. News. Tickets. Merchandise. Fan engagement. Action imagery.
82,Museum/Gallery,"gallery, museum",Minimalism + Motion-Driven,"Swiss Modernism 2.0, 3D & Hyperrealism",Storytelling-Driven + Feature-Rich,Visitor Analytics,Art-appropriate neutrals + Exhibition accents,Exhibitions. Collections. Tickets. Events. Virtual tours. Educational content. Art-focused design. 82,Museum/Gallery,"gallery, museum",Minimalism + Motion-Driven,"Swiss Modernism 2.0, 3D & Hyperrealism",Storytelling-Driven + Feature-Rich,Visitor Analytics,Art-appropriate neutrals + Exhibition accents,Exhibitions. Collections. Tickets. Events. Virtual tours. Educational content. Art-focused design.
83,Theater/Cinema,"cinema, theater",Dark Mode (OLED) + Motion-Driven,"Vibrant & Block-based, Glassmorphism",Hero-Centric Design + Conversion,Booking Analytics,Dark + Spotlight accents + Gold,Showtimes. Seat selection. Trailers. Coming soon. Membership. Dramatic imagery. 83,Theater/Cinema,"cinema, theater",Dark Mode (OLED) + Motion-Driven,"Vibrant & Block-based, Glassmorphism",Hero-Centric Design + Conversion,Booking Analytics,Dark + Spotlight accents + Gold,Showtimes. Seat selection. Trailers. Coming soon. Membership. Dramatic imagery.
84,Language Learning App,"app, language, learning",Claymorphism + Vibrant & Block-based,"Micro-interactions, Flat Design",Feature-Rich Showcase + Social Proof,Learning Analytics,Playful colors + Progress indicators + Country flags,Lesson structure. Progress tracking. Gamification. Speaking practice. Community. Achievement badges. 84,Language Learning App,"app, language, learning",Claymorphism + Vibrant & Block-based,"Micro-interactions, Flat Design",Feature-Rich Showcase + Social Proof,Learning Analytics,Playful colors + Progress indicators + Country flags,Lesson structure. Progress tracking. Gamification. Speaking practice. Community. Achievement badges.
85,Coding Bootcamp,"bootcamp, coding",Dark Mode (OLED) + Minimalism,"Cyberpunk UI, Flat Design",Feature-Rich Showcase + Social Proof,Student Analytics,Code editor colors + Brand + Success green,Curriculum. Projects. Career outcomes. Alumni. Pricing. Application. Terminal aesthetic. 85,Coding Bootcamp,"bootcamp, coding",Dark Mode (OLED) + Minimalism,"Cyberpunk UI, Flat Design",Feature-Rich Showcase + Social Proof,Student Analytics,Code editor colors + Brand + Success green,Curriculum. Projects. Career outcomes. Alumni. Pricing. Application. Terminal aesthetic.
86,Cybersecurity Platform,"cyber, security, platform",Cyberpunk UI + Dark Mode (OLED),"Neubrutalism, Minimal & Direct",Trust & Authority + Real-Time,Real-Time Monitoring + Heat Map,Matrix Green + Deep Black + Terminal feel,Data density. Threat visualization. Dark mode default. 86,Cybersecurity Platform,"cyber, security, platform",Cyberpunk UI + Dark Mode (OLED),"Neubrutalism, Minimal & Direct",Trust & Authority + Real-Time,Real-Time Monitoring + Heat Map,Matrix Green + Deep Black + Terminal feel,Data density. Threat visualization. Dark mode default.
87,Developer Tool / IDE,"dev, developer, tool, ide",Dark Mode (OLED) + Minimalism,"Flat Design, Bento Box Grid",Minimal & Direct + Documentation,Real-Time Monitor + Terminal,Dark syntax theme colors + Blue focus,Keyboard shortcuts. Syntax highlighting. Fast performance. 87,Developer Tool / IDE,"dev, developer, tool, ide",Dark Mode (OLED) + Minimalism,"Flat Design, Bento Box Grid",Minimal & Direct + Documentation,Real-Time Monitor + Terminal,Dark syntax theme colors + Blue focus,Keyboard shortcuts. Syntax highlighting. Fast performance.
88,Biotech / Life Sciences,"biotech, biology, science",Glassmorphism + Clean Science,"Minimalism, Organic Biophilic",Storytelling-Driven + Research,Data-Dense + Predictive,Sterile White + DNA Blue + Life Green,Data accuracy. Cleanliness. Complex data viz. 88,Biotech / Life Sciences,"biotech, biology, science",Glassmorphism + Clean Science,"Minimalism, Organic Biophilic",Storytelling-Driven + Research,Data-Dense + Predictive,Sterile White + DNA Blue + Life Green,Data accuracy. Cleanliness. Complex data viz.
89,Space Tech / Aerospace,"aerospace, space, tech",Holographic / HUD + Dark Mode,"Glassmorphism, 3D & Hyperrealism",Immersive Experience + Hero,Real-Time Monitoring + 3D,Deep Space Black + Star White + Metallic,High-tech feel. Precision. Telemetry data. 89,Space Tech / Aerospace,"aerospace, space, tech",Holographic / HUD + Dark Mode,"Glassmorphism, 3D & Hyperrealism",Immersive Experience + Hero,Real-Time Monitoring + 3D,Deep Space Black + Star White + Metallic,High-tech feel. Precision. Telemetry data.
90,Architecture / Interior,"architecture, design, interior",Exaggerated Minimalism + High Imagery,"Swiss Modernism 2.0, Parallax",Portfolio Grid + Visuals,Project Management + Gallery,Monochrome + Gold Accent + High Imagery,High-res images. Typography. Space. 90,Architecture / Interior,"architecture, design, interior",Exaggerated Minimalism + High Imagery,"Swiss Modernism 2.0, Parallax",Portfolio Grid + Visuals,Project Management + Gallery,Monochrome + Gold Accent + High Imagery,High-res images. Typography. Space.
91,Quantum Computing Interface,"quantum, computing, physics, qubit, future, science",Holographic / HUD + Dark Mode,"Glassmorphism, Spatial UI",Immersive/Interactive Experience,3D Spatial Data + Real-Time Monitor,Quantum Blue #00FFFF + Deep Black + Interference patterns,Visualize complexity. Qubit states. Probability clouds. High-tech trust. 91,Quantum Computing Interface,"quantum, computing, physics, qubit, future, science",Holographic / HUD + Dark Mode,"Glassmorphism, Spatial UI",Immersive/Interactive Experience,3D Spatial Data + Real-Time Monitor,Quantum Blue #00FFFF + Deep Black + Interference patterns,Visualize complexity. Qubit states. Probability clouds. High-tech trust.
92,Biohacking / Longevity App,"biohacking, health, longevity, tracking, wellness, science",Biomimetic / Organic 2.0,"Minimalism, Dark Mode (OLED)",Data-Dense + Storytelling,Real-Time Monitor + Biological Data,Cellular Pink/Red + DNA Blue + Clean White,Personal data privacy. Scientific credibility. Biological visualizations. 92,Biohacking / Longevity App,"biohacking, health, longevity, tracking, wellness, science",Biomimetic / Organic 2.0,"Minimalism, Dark Mode (OLED)",Data-Dense + Storytelling,Real-Time Monitor + Biological Data,Cellular Pink/Red + DNA Blue + Clean White,Personal data privacy. Scientific credibility. Biological visualizations.
93,Autonomous Drone Fleet Manager,"drone, autonomous, fleet, aerial, logistics, robotics",HUD / Sci-Fi FUI,"Real-Time Monitor, Spatial UI",Real-Time Monitor,Geographic + Real-Time,Tactical Green #00FF00 + Alert Red + Map Dark,Real-time telemetry. 3D spatial awareness. Latency indicators. Safety alerts. 93,Autonomous Drone Fleet Manager,"drone, autonomous, fleet, aerial, logistics, robotics",HUD / Sci-Fi FUI,"Real-Time Monitor, Spatial UI",Real-Time Monitor,Geographic + Real-Time,Tactical Green #00FF00 + Alert Red + Map Dark,Real-time telemetry. 3D spatial awareness. Latency indicators. Safety alerts.
94,Generative Art Platform,"art, generative, ai, creative, platform, gallery",Minimalism (Frame) + Gen Z Chaos,"Masonry Grid, Dark Mode",Bento Grid Showcase,Gallery / Portfolio,Neutral #F5F5F5 (Canvas) + User Content,Content is king. Fast loading. Creator attribution. Minting flow. 94,Generative Art Platform,"art, generative, ai, creative, platform, gallery",Minimalism (Frame) + Gen Z Chaos,"Masonry Grid, Dark Mode",Bento Grid Showcase,Gallery / Portfolio,Neutral #F5F5F5 (Canvas) + User Content,Content is king. Fast loading. Creator attribution. Minting flow.
95,Spatial Computing OS / App,"spatial, vr, ar, vision, os, immersive, mixed-reality",Spatial UI (VisionOS),"Glassmorphism, 3D & Hyperrealism",Immersive/Interactive Experience,Spatial Dashboard,Frosted Glass + System Colors + Depth,Gaze/Pinch interaction. Depth hierarchy. Environment awareness. 95,Spatial Computing OS / App,"spatial, vr, ar, vision, os, immersive, mixed-reality",Spatial UI (VisionOS),"Glassmorphism, 3D & Hyperrealism",Immersive/Interactive Experience,Spatial Dashboard,Frosted Glass + System Colors + Depth,Gaze/Pinch interaction. Depth hierarchy. Environment awareness.
96,Sustainable Energy / Climate Tech,"climate, energy, sustainable, green, tech, carbon",Organic Biophilic + E-Ink / Paper,"Data-Dense, Swiss Modernism",Interactive Demo + Data,Energy/Utilities Dashboard,Earth Green + Sky Blue + Solar Yellow,Data transparency. Impact visualization. Low-carbon web design. 96,Sustainable Energy / Climate Tech,"climate, energy, sustainable, green, tech, carbon",Organic Biophilic + E-Ink / Paper,"Data-Dense, Swiss Modernism",Interactive Demo + Data,Energy/Utilities Dashboard,Earth Green + Sky Blue + Solar Yellow,Data transparency. Impact visualization. Low-carbon web design.
1 No Product Type Keywords Primary Style Recommendation Secondary Styles Landing Page Pattern Dashboard Style (if applicable) Color Palette Focus Key Considerations
2 1 SaaS (General) app, b2b, cloud, general, saas, software, subscription Glassmorphism + Flat Design Soft UI Evolution, Minimalism Hero + Features + CTA Data-Dense + Real-Time Monitoring Trust blue + accent contrast Balance modern feel with clarity. Focus on CTAs.
3 2 Micro SaaS app, b2b, cloud, indie, micro, micro-saas, niche, saas, small, software, solo, subscription Flat Design + Vibrant & Block Motion-Driven, Micro-interactions Minimal & Direct + Demo Executive Dashboard Vibrant primary + white space Keep simple, show product quickly. Speed is key.
4 3 E-commerce buy, commerce, e, ecommerce, products, retail, sell, shop, store Vibrant & Block-based Aurora UI, Motion-Driven Feature-Rich Showcase Sales Intelligence Dashboard Brand primary + success green Engagement & conversions. High visual hierarchy.
5 4 E-commerce Luxury buy, commerce, e, ecommerce, elegant, exclusive, high-end, luxury, premium, products, retail, sell, shop, store Liquid Glass + Glassmorphism 3D & Hyperrealism, Aurora UI Feature-Rich Showcase Sales Intelligence Dashboard Premium colors + minimal accent Elegance & sophistication. Premium materials.
6 5 Service Landing Page appointment, booking, consultation, conversion, landing, marketing, page, service Hero-Centric + Trust & Authority Social Proof-Focused, Storytelling Hero-Centric Design N/A - Analytics for conversions Brand primary + trust colors Social proof essential. Show expertise.
7 6 B2B Service appointment, b, b2b, booking, business, consultation, corporate, enterprise, service Trust & Authority + Minimal Feature-Rich, Conversion-Optimized Feature-Rich Showcase Sales Intelligence Dashboard Professional blue + neutral grey Credibility essential. Clear ROI messaging.
8 7 Financial Dashboard admin, analytics, dashboard, data, financial, panel Dark Mode (OLED) + Data-Dense Minimalism, Accessible & Ethical N/A - Dashboard focused Financial Dashboard Dark bg + red/green alerts + trust blue High contrast, real-time updates, accuracy paramount.
9 8 Analytics Dashboard admin, analytics, dashboard, data, panel Data-Dense + Heat Map & Heatmap Minimalism, Dark Mode (OLED) N/A - Analytics focused Drill-Down Analytics + Comparative Cool→Hot gradients + neutral grey Clarity > aesthetics. Color-coded data priority.
10 9 Healthcare App app, clinic, health, healthcare, medical, patient Neumorphism + Accessible & Ethical Soft UI Evolution, Claymorphism (for patients) Social Proof-Focused User Behavior Analytics Calm blue + health green + trust Accessibility mandatory. Calming aesthetic.
11 10 Educational App app, course, education, educational, learning, school, training Claymorphism + Micro-interactions Vibrant & Block-based, Flat Design Storytelling-Driven User Behavior Analytics Playful colors + clear hierarchy Engagement & ease of use. Age-appropriate design.
12 11 Creative Agency agency, creative, design, marketing, studio Brutalism + Motion-Driven Retro-Futurism, Storytelling-Driven Storytelling-Driven N/A - Portfolio focused Bold primaries + artistic freedom Differentiation key. Wow-factor necessary.
13 12 Portfolio/Personal creative, personal, portfolio, projects, showcase, work Motion-Driven + Minimalism Brutalism, Aurora UI Storytelling-Driven N/A - Personal branding Brand primary + artistic interpretation Showcase work. Personality shine through.
14 13 Gaming entertainment, esports, game, gaming, play 3D & Hyperrealism + Retro-Futurism Motion-Driven, Vibrant & Block Feature-Rich Showcase N/A - Game focused Vibrant + neon + immersive colors Immersion priority. Performance critical.
15 14 Government/Public Service appointment, booking, consultation, government, public, service Accessible & Ethical + Minimalism Flat Design, Inclusive Design Minimal & Direct Executive Dashboard Professional blue + high contrast WCAG AAA mandatory. Trust paramount.
16 15 Fintech/Crypto banking, blockchain, crypto, defi, finance, fintech, money, nft, payment, web3 Glassmorphism + Dark Mode (OLED) Retro-Futurism, Motion-Driven Conversion-Optimized Real-Time Monitoring + Predictive Dark tech colors + trust + vibrant accents Security perception. Real-time data critical.
17 16 Social Media App app, community, content, entertainment, media, network, sharing, social, streaming, users, video Vibrant & Block-based + Motion-Driven Aurora UI, Micro-interactions Feature-Rich Showcase User Behavior Analytics Vibrant + engagement colors Engagement & retention. Addictive design ethics.
18 17 Productivity Tool collaboration, productivity, project, task, tool, workflow Flat Design + Micro-interactions Minimalism, Soft UI Evolution Interactive Product Demo Drill-Down Analytics Clear hierarchy + functional colors Ease of use. Speed & efficiency focus.
19 18 Design System/Component Library component, design, library, system Minimalism + Accessible & Ethical Flat Design, Zero Interface Feature-Rich Showcase N/A - Dev focused Clear hierarchy + code-like structure Consistency. Developer-first approach.
20 19 AI/Chatbot Platform ai, artificial-intelligence, automation, chatbot, machine-learning, ml, platform AI-Native UI + Minimalism Zero Interface, Glassmorphism Interactive Product Demo AI/ML Analytics Dashboard Neutral + AI Purple (#6366F1) Conversational UI. Streaming text. Context awareness. Minimal chrome.
21 20 NFT/Web3 Platform nft, platform, web Cyberpunk UI + Glassmorphism Aurora UI, 3D & Hyperrealism Feature-Rich Showcase Crypto/Blockchain Dashboard Dark + Neon + Gold (#FFD700) Wallet integration. Transaction feedback. Gas fees display. Dark mode essential.
22 21 Creator Economy Platform creator, economy, platform Vibrant & Block-based + Bento Box Grid Motion-Driven, Aurora UI Social Proof-Focused User Behavior Analytics Vibrant + Brand colors Creator profiles. Monetization display. Engagement metrics. Social proof.
23 22 Sustainability/ESG Platform ai, artificial-intelligence, automation, esg, machine-learning, ml, platform, sustainability Organic Biophilic + Minimalism Accessible & Ethical, Flat Design Trust & Authority Energy/Utilities Dashboard Green (#228B22) + Earth tones Carbon footprint visuals. Progress indicators. Certification badges. Eco-friendly imagery.
24 23 Remote Work/Collaboration Tool collaboration, remote, tool, work Soft UI Evolution + Minimalism Glassmorphism, Micro-interactions Feature-Rich Showcase Drill-Down Analytics Calm Blue + Neutral grey Real-time collaboration. Status indicators. Video integration. Notification management.
25 24 Mental Health App app, health, mental Neumorphism + Accessible & Ethical Claymorphism, Soft UI Evolution Social Proof-Focused Healthcare Analytics Calm Pastels + Trust colors Calming aesthetics. Privacy-first. Crisis resources. Progress tracking. Accessibility mandatory.
26 25 Pet Tech App app, pet, tech Claymorphism + Vibrant & Block-based Micro-interactions, Flat Design Storytelling-Driven User Behavior Analytics Playful + Warm colors Pet profiles. Health tracking. Playful UI. Photo galleries. Vet integration.
27 26 Smart Home/IoT Dashboard admin, analytics, dashboard, data, home, iot, panel, smart Glassmorphism + Dark Mode (OLED) Minimalism, AI-Native UI Interactive Product Demo Real-Time Monitoring Dark + Status indicator colors Device status. Real-time controls. Energy monitoring. Automation rules. Quick actions.
28 27 EV/Charging Ecosystem charging, ecosystem, ev Minimalism + Aurora UI Glassmorphism, Organic Biophilic Hero-Centric Design Energy/Utilities Dashboard Electric Blue (#009CD1) + Green Charging station maps. Range estimation. Cost calculation. Environmental impact.
29 28 Subscription Box Service appointment, booking, box, consultation, membership, plan, recurring, service, subscription Vibrant & Block-based + Motion-Driven Claymorphism, Aurora UI Feature-Rich Showcase E-commerce Analytics Brand + Excitement colors Unboxing experience. Personalization quiz. Subscription management. Product reveals.
30 29 Podcast Platform platform, podcast Dark Mode (OLED) + Minimalism Motion-Driven, Vibrant & Block-based Storytelling-Driven Media/Entertainment Dashboard Dark + Audio waveform accents Audio player UX. Episode discovery. Creator tools. Analytics for podcasters.
31 30 Dating App app, dating Vibrant & Block-based + Motion-Driven Aurora UI, Glassmorphism Social Proof-Focused User Behavior Analytics Warm + Romantic (Pink/Red gradients) Profile cards. Swipe interactions. Match animations. Safety features. Video chat.
32 31 Micro-Credentials/Badges Platform badges, credentials, micro, platform Minimalism + Flat Design Accessible & Ethical, Swiss Modernism 2.0 Trust & Authority Education Dashboard Trust Blue + Gold (#FFD700) Credential verification. Badge display. Progress tracking. Issuer trust. LinkedIn integration.
33 32 Knowledge Base/Documentation base, documentation, knowledge Minimalism + Accessible & Ethical Swiss Modernism 2.0, Flat Design FAQ/Documentation N/A - Documentation focused Clean hierarchy + minimal color Search-first. Clear navigation. Code highlighting. Version switching. Feedback system.
34 33 Hyperlocal Services appointment, booking, consultation, hyperlocal, service, services Minimalism + Vibrant & Block-based Micro-interactions, Flat Design Conversion-Optimized Drill-Down Analytics + Map Location markers + Trust colors Map integration. Service categories. Provider profiles. Booking system. Reviews.
35 34 Beauty/Spa/Wellness Service appointment, beauty, booking, consultation, service, spa, wellness Soft UI Evolution + Neumorphism Glassmorphism, Minimalism Hero-Centric Design + Social Proof User Behavior Analytics Soft pastels (Pink #FFB6C1 Sage #90EE90) + Cream + Gold accents Calming aesthetic. Booking system. Service menu. Before/after gallery. Testimonials. Relaxing imagery.
36 35 Luxury/Premium Brand brand, elegant, exclusive, high-end, luxury, premium Liquid Glass + Glassmorphism Minimalism, 3D & Hyperrealism Storytelling-Driven + Feature-Rich Sales Intelligence Dashboard Black + Gold (#FFD700) + White + Minimal accent Elegance paramount. Premium imagery. Storytelling. High-quality visuals. Exclusive feel.
37 36 Restaurant/Food Service appointment, booking, consultation, delivery, food, menu, order, restaurant, service Vibrant & Block-based + Motion-Driven Claymorphism, Flat Design Hero-Centric Design + Conversion N/A - Booking focused Warm colors (Orange Red Brown) + appetizing imagery Menu display. Online ordering. Reservation system. Food photography. Location/hours prominent.
38 37 Fitness/Gym App app, exercise, fitness, gym, health, workout Vibrant & Block-based + Dark Mode (OLED) Motion-Driven, Neumorphism Feature-Rich Showcase User Behavior Analytics Energetic (Orange #FF6B35 Electric Blue) + Dark bg Progress tracking. Workout plans. Community features. Achievements. Motivational design.
39 38 Real Estate/Property buy, estate, housing, property, real, real-estate, rent Glassmorphism + Minimalism Motion-Driven, 3D & Hyperrealism Hero-Centric Design + Feature-Rich Sales Intelligence Dashboard Trust Blue (#0077B6) + Gold accents + White Property listings. Virtual tours. Map integration. Agent profiles. Mortgage calculator. High-quality imagery.
40 39 Travel/Tourism Agency agency, booking, creative, design, flight, hotel, marketing, studio, tourism, travel, vacation Aurora UI + Motion-Driven Vibrant & Block-based, Glassmorphism Storytelling-Driven + Hero-Centric Booking Analytics Vibrant destination colors + Sky Blue + Warm accents Destination showcase. Booking system. Itinerary builder. Reviews. Inspiration galleries. Mobile-first.
41 40 Hotel/Hospitality hospitality, hotel Liquid Glass + Minimalism Glassmorphism, Soft UI Evolution Hero-Centric Design + Social Proof Revenue Management Dashboard Warm neutrals + Gold (#D4AF37) + Brand accent Room booking. Amenities showcase. Location maps. Guest reviews. Seasonal pricing. Luxury imagery.
42 41 Wedding/Event Planning conference, event, meetup, planning, registration, ticket, wedding Soft UI Evolution + Aurora UI Glassmorphism, Motion-Driven Storytelling-Driven + Social Proof N/A - Planning focused Soft Pink (#FFD6E0) + Gold + Cream + Sage Portfolio gallery. Vendor directory. Planning tools. Timeline. Budget tracker. Romantic aesthetic.
43 42 Legal Services appointment, attorney, booking, compliance, consultation, contract, law, legal, service, services Trust & Authority + Minimalism Accessible & Ethical, Swiss Modernism 2.0 Trust & Authority + Minimal Case Management Dashboard Navy Blue (#1E3A5F) + Gold + White Credibility paramount. Practice areas. Attorney profiles. Case results. Contact forms. Professional imagery.
44 43 Insurance Platform insurance, platform Trust & Authority + Flat Design Accessible & Ethical, Minimalism Conversion-Optimized + Trust Claims Analytics Dashboard Trust Blue (#0066CC) + Green (security) + Neutral Quote calculator. Policy comparison. Claims process. Trust signals. Clear pricing. Security badges.
45 44 Banking/Traditional Finance banking, finance, traditional Minimalism + Accessible & Ethical Trust & Authority, Dark Mode (OLED) Trust & Authority + Feature-Rich Financial Dashboard Navy (#0A1628) + Trust Blue + Gold accents Security-first. Account overview. Transaction history. Mobile banking. Accessibility critical. Trust paramount.
46 45 Online Course/E-learning course, e, learning, online Claymorphism + Vibrant & Block-based Motion-Driven, Flat Design Feature-Rich Showcase + Social Proof Education Dashboard Vibrant learning colors + Progress green Course catalog. Progress tracking. Video player. Quizzes. Certificates. Community forums. Gamification.
47 46 Non-profit/Charity charity, non, profit Accessible & Ethical + Organic Biophilic Minimalism, Storytelling-Driven Storytelling-Driven + Trust Donation Analytics Dashboard Cause-related colors + Trust + Warm Impact stories. Donation flow. Transparency reports. Volunteer signup. Event calendar. Emotional connection.
48 47 Music Streaming music, streaming Dark Mode (OLED) + Vibrant & Block-based Motion-Driven, Aurora UI Feature-Rich Showcase Media/Entertainment Dashboard Dark (#121212) + Vibrant accents + Album art colors Audio player. Playlist management. Artist pages. Personalization. Social features. Waveform visualizations.
49 48 Video Streaming/OTT ott, streaming, video Dark Mode (OLED) + Motion-Driven Glassmorphism, Vibrant & Block-based Hero-Centric Design + Feature-Rich Media/Entertainment Dashboard Dark bg + Content poster colors + Brand accent Video player. Content discovery. Watchlist. Continue watching. Personalized recommendations. Thumbnail-heavy.
50 49 Job Board/Recruitment board, job, recruitment Flat Design + Minimalism Vibrant & Block-based, Accessible & Ethical Conversion-Optimized + Feature-Rich HR Analytics Dashboard Professional Blue + Success Green + Neutral Job listings. Search/filter. Company profiles. Application tracking. Resume upload. Salary insights.
51 50 Marketplace (P2P) buyers, listings, marketplace, p, platform, sellers Vibrant & Block-based + Flat Design Micro-interactions, Trust & Authority Feature-Rich Showcase + Social Proof E-commerce Analytics Trust colors + Category colors + Success green Seller/buyer profiles. Listings. Reviews/ratings. Secure payment. Messaging. Search/filter. Trust badges.
52 51 Logistics/Delivery delivery, logistics Minimalism + Flat Design Dark Mode (OLED), Micro-interactions Feature-Rich Showcase + Conversion Real-Time Monitoring + Route Analytics Blue (#2563EB) + Orange (tracking) + Green (delivered) Real-time tracking. Delivery scheduling. Route optimization. Driver management. Status updates. Map integration.
53 52 Agriculture/Farm Tech agriculture, farm, tech Organic Biophilic + Flat Design Minimalism, Accessible & Ethical Feature-Rich Showcase + Trust IoT Sensor Dashboard Earth Green (#4A7C23) + Brown + Sky Blue Crop monitoring. Weather data. IoT sensors. Yield tracking. Market prices. Sustainable imagery.
54 53 Construction/Architecture architecture, construction Minimalism + 3D & Hyperrealism Brutalism, Swiss Modernism 2.0 Hero-Centric Design + Feature-Rich Project Management Dashboard Grey (#4A4A4A) + Orange (safety) + Blueprint Blue Project portfolio. 3D renders. Timeline. Material specs. Team collaboration. Blueprint aesthetic.
55 54 Automotive/Car Dealership automotive, car, dealership Motion-Driven + 3D & Hyperrealism Dark Mode (OLED), Glassmorphism Hero-Centric Design + Feature-Rich Sales Intelligence Dashboard Brand colors + Metallic accents + Dark/Light Vehicle showcase. 360° views. Comparison tools. Financing calculator. Test drive booking. High-quality imagery.
56 55 Photography Studio photography, studio Motion-Driven + Minimalism Aurora UI, Glassmorphism Storytelling-Driven + Hero-Centric N/A - Portfolio focused Black + White + Minimal accent Portfolio gallery. Before/after. Service packages. Booking system. Client galleries. Full-bleed imagery.
57 56 Coworking Space coworking, space Vibrant & Block-based + Glassmorphism Minimalism, Motion-Driven Hero-Centric Design + Feature-Rich Occupancy Dashboard Energetic colors + Wood tones + Brand accent Space tour. Membership plans. Booking system. Amenities. Community events. Virtual tour.
58 57 Cleaning Service appointment, booking, cleaning, consultation, service Soft UI Evolution + Flat Design Minimalism, Micro-interactions Conversion-Optimized + Trust Service Analytics Fresh Blue (#00B4D8) + Clean White + Green Service packages. Booking system. Price calculator. Before/after gallery. Reviews. Trust badges.
59 58 Home Services (Plumber/Electrician) appointment, booking, consultation, electrician, home, plumber, service, services Flat Design + Trust & Authority Minimalism, Accessible & Ethical Conversion-Optimized + Trust Service Analytics Trust Blue + Safety Orange + Professional grey Service list. Emergency contact. Booking. Price transparency. Certifications. Local trust signals.
60 59 Childcare/Daycare childcare, daycare Claymorphism + Vibrant & Block-based Soft UI Evolution, Accessible & Ethical Social Proof-Focused + Trust Parent Dashboard Playful pastels + Safe colors + Warm accents Programs. Staff profiles. Safety certifications. Parent portal. Activity updates. Cheerful imagery.
61 60 Senior Care/Elderly care, elderly, senior Accessible & Ethical + Soft UI Evolution Minimalism, Neumorphism Trust & Authority + Social Proof Healthcare Analytics Calm Blue + Warm neutrals + Large text Care services. Staff qualifications. Facility tour. Family portal. Large touch targets. High contrast. Accessibility-first.
62 61 Medical Clinic clinic, medical Accessible & Ethical + Minimalism Neumorphism, Trust & Authority Trust & Authority + Conversion Healthcare Analytics Medical Blue (#0077B6) + Trust White + Calm Green Services. Doctor profiles. Online booking. Patient portal. Insurance info. HIPAA compliant. Trust signals.
63 62 Pharmacy/Drug Store drug, pharmacy, store Flat Design + Accessible & Ethical Minimalism, Trust & Authority Conversion-Optimized + Trust Inventory Dashboard Pharmacy Green + Trust Blue + Clean White Product catalog. Prescription upload. Refill reminders. Health info. Store locator. Safety certifications.
64 63 Dental Practice dental, practice Soft UI Evolution + Minimalism Accessible & Ethical, Trust & Authority Social Proof-Focused + Conversion Patient Analytics Fresh Blue + White + Smile Yellow accent Services. Dentist profiles. Before/after. Online booking. Insurance. Patient testimonials. Friendly imagery.
65 64 Veterinary Clinic clinic, veterinary Claymorphism + Accessible & Ethical Soft UI Evolution, Flat Design Social Proof-Focused + Trust Pet Health Dashboard Caring Blue + Pet-friendly colors + Warm accents Pet services. Vet profiles. Online booking. Pet portal. Emergency info. Friendly animal imagery.
66 65 Florist/Plant Shop florist, plant, shop Organic Biophilic + Vibrant & Block-based Aurora UI, Motion-Driven Hero-Centric Design + Conversion E-commerce Analytics Natural Green + Floral pinks/purples + Earth tones Product catalog. Occasion categories. Delivery scheduling. Care guides. Seasonal collections. Beautiful imagery.
67 66 Bakery/Cafe bakery, cafe Vibrant & Block-based + Soft UI Evolution Claymorphism, Motion-Driven Hero-Centric Design + Conversion N/A - Order focused Warm Brown + Cream + Appetizing accents Menu display. Online ordering. Location/hours. Catering. Seasonal specials. Appetizing photography.
68 67 Coffee Shop coffee, shop Minimalism + Organic Biophilic Soft UI Evolution, Flat Design Hero-Centric Design + Conversion N/A - Order focused Coffee Brown (#6F4E37) + Cream + Warm accents Menu. Online ordering. Loyalty program. Location. Story/origin. Cozy aesthetic.
69 68 Brewery/Winery brewery, winery Motion-Driven + Storytelling-Driven Dark Mode (OLED), Organic Biophilic Storytelling-Driven + Hero-Centric N/A - E-commerce focused Deep amber/burgundy + Gold + Craft aesthetic Product showcase. Story/heritage. Tasting notes. Events. Club membership. Artisanal imagery.
70 69 Airline ai, airline, artificial-intelligence, automation, machine-learning, ml Minimalism + Glassmorphism Motion-Driven, Accessible & Ethical Conversion-Optimized + Feature-Rich Operations Dashboard Sky Blue + Brand colors + Trust accents Flight search. Booking. Check-in. Boarding pass. Loyalty program. Route maps. Mobile-first.
71 70 News/Media Platform content, entertainment, media, news, platform, streaming, video Minimalism + Flat Design Dark Mode (OLED), Accessible & Ethical Hero-Centric Design + Feature-Rich Media Analytics Dashboard Brand colors + High contrast + Category colors Article layout. Breaking news. Categories. Search. Subscription. Mobile reading. Fast loading.
72 71 Magazine/Blog articles, blog, content, magazine, posts, writing Swiss Modernism 2.0 + Motion-Driven Minimalism, Aurora UI Storytelling-Driven + Hero-Centric Content Analytics Editorial colors + Brand primary + Clean white Article showcase. Category navigation. Author profiles. Newsletter signup. Related content. Typography-focused.
73 72 Freelancer Platform freelancer, platform Flat Design + Minimalism Vibrant & Block-based, Micro-interactions Feature-Rich Showcase + Conversion Marketplace Analytics Professional Blue + Success Green + Neutral Profile creation. Portfolio. Skill matching. Messaging. Payment. Reviews. Project management.
74 73 Consulting Firm consulting, firm Trust & Authority + Minimalism Swiss Modernism 2.0, Accessible & Ethical Trust & Authority + Feature-Rich N/A - Lead generation Navy + Gold + Professional grey Service areas. Case studies. Team profiles. Thought leadership. Contact. Professional credibility.
75 74 Marketing Agency agency, creative, design, marketing, studio Brutalism + Motion-Driven Vibrant & Block-based, Aurora UI Storytelling-Driven + Feature-Rich Campaign Analytics Bold brand colors + Creative freedom Portfolio. Case studies. Services. Team. Creative showcase. Results-focused. Bold aesthetic.
76 75 Event Management conference, event, management, meetup, registration, ticket Vibrant & Block-based + Motion-Driven Glassmorphism, Aurora UI Hero-Centric Design + Feature-Rich Event Analytics Event theme colors + Excitement accents Event showcase. Registration. Agenda. Speakers. Sponsors. Ticket sales. Countdown timer.
77 76 Conference/Webinar Platform conference, platform, webinar Glassmorphism + Minimalism Motion-Driven, Flat Design Feature-Rich Showcase + Conversion Attendee Analytics Professional Blue + Video accent + Brand Registration. Agenda. Speaker profiles. Live stream. Networking. Recording access. Virtual event features.
78 77 Membership/Community community, membership Vibrant & Block-based + Soft UI Evolution Bento Box Grid, Micro-interactions Social Proof-Focused + Conversion Community Analytics Community brand colors + Engagement accents Member benefits. Pricing tiers. Community showcase. Events. Member directory. Exclusive content.
79 78 Newsletter Platform newsletter, platform Minimalism + Flat Design Swiss Modernism 2.0, Accessible & Ethical Minimal & Direct + Conversion Email Analytics Brand primary + Clean white + CTA accent Subscribe form. Archive. About. Social proof. Sample content. Simple conversion.
80 79 Digital Products/Downloads digital, downloads, products Vibrant & Block-based + Motion-Driven Glassmorphism, Bento Box Grid Feature-Rich Showcase + Conversion E-commerce Analytics Product category colors + Brand + Success green Product showcase. Preview. Pricing. Instant delivery. License management. Customer reviews.
81 80 Church/Religious Organization church, organization, religious Accessible & Ethical + Soft UI Evolution Minimalism, Trust & Authority Hero-Centric Design + Social Proof N/A - Community focused Warm Gold + Deep Purple/Blue + White Service times. Events. Sermons. Community. Giving. Location. Welcoming imagery.
82 81 Sports Team/Club club, sports, team Vibrant & Block-based + Motion-Driven Dark Mode (OLED), 3D & Hyperrealism Hero-Centric Design + Feature-Rich Performance Analytics Team colors + Energetic accents Schedule. Roster. News. Tickets. Merchandise. Fan engagement. Action imagery.
83 82 Museum/Gallery gallery, museum Minimalism + Motion-Driven Swiss Modernism 2.0, 3D & Hyperrealism Storytelling-Driven + Feature-Rich Visitor Analytics Art-appropriate neutrals + Exhibition accents Exhibitions. Collections. Tickets. Events. Virtual tours. Educational content. Art-focused design.
84 83 Theater/Cinema cinema, theater Dark Mode (OLED) + Motion-Driven Vibrant & Block-based, Glassmorphism Hero-Centric Design + Conversion Booking Analytics Dark + Spotlight accents + Gold Showtimes. Seat selection. Trailers. Coming soon. Membership. Dramatic imagery.
85 84 Language Learning App app, language, learning Claymorphism + Vibrant & Block-based Micro-interactions, Flat Design Feature-Rich Showcase + Social Proof Learning Analytics Playful colors + Progress indicators + Country flags Lesson structure. Progress tracking. Gamification. Speaking practice. Community. Achievement badges.
86 85 Coding Bootcamp bootcamp, coding Dark Mode (OLED) + Minimalism Cyberpunk UI, Flat Design Feature-Rich Showcase + Social Proof Student Analytics Code editor colors + Brand + Success green Curriculum. Projects. Career outcomes. Alumni. Pricing. Application. Terminal aesthetic.
87 86 Cybersecurity Platform cyber, security, platform Cyberpunk UI + Dark Mode (OLED) Neubrutalism, Minimal & Direct Trust & Authority + Real-Time Real-Time Monitoring + Heat Map Matrix Green + Deep Black + Terminal feel Data density. Threat visualization. Dark mode default.
88 87 Developer Tool / IDE dev, developer, tool, ide Dark Mode (OLED) + Minimalism Flat Design, Bento Box Grid Minimal & Direct + Documentation Real-Time Monitor + Terminal Dark syntax theme colors + Blue focus Keyboard shortcuts. Syntax highlighting. Fast performance.
89 88 Biotech / Life Sciences biotech, biology, science Glassmorphism + Clean Science Minimalism, Organic Biophilic Storytelling-Driven + Research Data-Dense + Predictive Sterile White + DNA Blue + Life Green Data accuracy. Cleanliness. Complex data viz.
90 89 Space Tech / Aerospace aerospace, space, tech Holographic / HUD + Dark Mode Glassmorphism, 3D & Hyperrealism Immersive Experience + Hero Real-Time Monitoring + 3D Deep Space Black + Star White + Metallic High-tech feel. Precision. Telemetry data.
91 90 Architecture / Interior architecture, design, interior Exaggerated Minimalism + High Imagery Swiss Modernism 2.0, Parallax Portfolio Grid + Visuals Project Management + Gallery Monochrome + Gold Accent + High Imagery High-res images. Typography. Space.
92 91 Quantum Computing Interface quantum, computing, physics, qubit, future, science Holographic / HUD + Dark Mode Glassmorphism, Spatial UI Immersive/Interactive Experience 3D Spatial Data + Real-Time Monitor Quantum Blue #00FFFF + Deep Black + Interference patterns Visualize complexity. Qubit states. Probability clouds. High-tech trust.
93 92 Biohacking / Longevity App biohacking, health, longevity, tracking, wellness, science Biomimetic / Organic 2.0 Minimalism, Dark Mode (OLED) Data-Dense + Storytelling Real-Time Monitor + Biological Data Cellular Pink/Red + DNA Blue + Clean White Personal data privacy. Scientific credibility. Biological visualizations.
94 93 Autonomous Drone Fleet Manager drone, autonomous, fleet, aerial, logistics, robotics HUD / Sci-Fi FUI Real-Time Monitor, Spatial UI Real-Time Monitor Geographic + Real-Time Tactical Green #00FF00 + Alert Red + Map Dark Real-time telemetry. 3D spatial awareness. Latency indicators. Safety alerts.
95 94 Generative Art Platform art, generative, ai, creative, platform, gallery Minimalism (Frame) + Gen Z Chaos Masonry Grid, Dark Mode Bento Grid Showcase Gallery / Portfolio Neutral #F5F5F5 (Canvas) + User Content Content is king. Fast loading. Creator attribution. Minting flow.
96 95 Spatial Computing OS / App spatial, vr, ar, vision, os, immersive, mixed-reality Spatial UI (VisionOS) Glassmorphism, 3D & Hyperrealism Immersive/Interactive Experience Spatial Dashboard Frosted Glass + System Colors + Depth Gaze/Pinch interaction. Depth hierarchy. Environment awareness.
97 96 Sustainable Energy / Climate Tech climate, energy, sustainable, green, tech, carbon Organic Biophilic + E-Ink / Paper Data-Dense, Swiss Modernism Interactive Demo + Data Energy/Utilities Dashboard Earth Green + Sky Blue + Solar Yellow Data transparency. Impact visualization. Low-carbon web design.

View File

@@ -1,23 +1,23 @@
STT,Style Category,AI Prompt Keywords (Copy-Paste Ready),CSS/Technical Keywords,Implementation Checklist,Design System Variables STT,Style Category,AI Prompt Keywords (Copy-Paste Ready),CSS/Technical Keywords,Implementation Checklist,Design System Variables
1,Minimalism & Swiss Style,"Design a minimalist landing page. Use: white space, geometric layouts, sans-serif fonts, high contrast, grid-based structure, essential elements only. Avoid shadows and gradients. Focus on clarity and functionality.","display: grid, gap: 2rem, font-family: sans-serif, color: #000 or #FFF, max-width: 1200px, clean borders, no box-shadow unless necessary","☐ Grid-based layout 12-16 columns, ☐ Typography hierarchy clear, ☐ No unnecessary decorations, ☐ WCAG AAA contrast verified, ☐ Mobile responsive grid","--spacing: 2rem, --border-radius: 0px, --font-weight: 400-700, --shadow: none, --accent-color: single primary only" 1,Minimalism & Swiss Style,"Design a minimalist landing page. Use: white space, geometric layouts, sans-serif fonts, high contrast, grid-based structure, essential elements only. Avoid shadows and gradients. Focus on clarity and functionality.","display: grid, gap: 2rem, font-family: sans-serif, color: #000 or #FFF, max-width: 1200px, clean borders, no box-shadow unless necessary","☐ Grid-based layout 12-16 columns, ☐ Typography hierarchy clear, ☐ No unnecessary decorations, ☐ WCAG AAA contrast verified, ☐ Mobile responsive grid","--spacing: 2rem, --border-radius: 0px, --font-weight: 400-700, --shadow: none, --accent-color: single primary only"
2,Neumorphism,"Create a neumorphic UI with soft 3D effects. Use light pastels, rounded corners (12-16px), subtle soft shadows (multiple layers), no hard lines, monochromatic color scheme with light/dark variations. Embossed/debossed effect on interactive elements.","border-radius: 12-16px, box-shadow: -5px -5px 15px rgba(0,0,0,0.1), 5px 5px 15px rgba(255,255,255,0.8), background: linear-gradient(145deg, color1, color2), transform: scale on press","☐ Rounded corners 12-16px consistent, ☐ Multiple shadow layers (2-3), ☐ Pastel color verified, ☐ Monochromatic palette checked, ☐ Press animation smooth 150ms","--border-radius: 14px, --shadow-soft-1: -5px -5px 15px, --shadow-soft-2: 5px 5px 15px, --color-light: #F5F5F5, --color-primary: single pastel" 2,Neumorphism,"Create a neumorphic UI with soft 3D effects. Use light pastels, rounded corners (12-16px), subtle soft shadows (multiple layers), no hard lines, monochromatic color scheme with light/dark variations. Embossed/debossed effect on interactive elements.","border-radius: 12-16px, box-shadow: -5px -5px 15px rgba(0,0,0,0.1), 5px 5px 15px rgba(255,255,255,0.8), background: linear-gradient(145deg, color1, color2), transform: scale on press","☐ Rounded corners 12-16px consistent, ☐ Multiple shadow layers (2-3), ☐ Pastel color verified, ☐ Monochromatic palette checked, ☐ Press animation smooth 150ms","--border-radius: 14px, --shadow-soft-1: -5px -5px 15px, --shadow-soft-2: 5px 5px 15px, --color-light: #F5F5F5, --color-primary: single pastel"
3,Glassmorphism,"Design a glassmorphic interface with frosted glass effect. Use backdrop blur (10-20px), translucent overlays (rgba 10-30% opacity), vibrant background colors, subtle borders, light source reflection, layered depth. Perfect for modern overlays and cards.","backdrop-filter: blur(15px), background: rgba(255, 255, 255, 0.15), border: 1px solid rgba(255,255,255,0.2), -webkit-backdrop-filter: blur(15px), z-index layering for depth","☐ Backdrop-filter blur 10-20px, ☐ Translucent white 15-30% opacity, ☐ Subtle border 1px light, ☐ Vibrant background verified, ☐ Text contrast 4.5:1 checked","--blur-amount: 15px, --glass-opacity: 0.15, --border-color: rgba(255,255,255,0.2), --background: vibrant color, --text-color: light/dark based on BG" 3,Glassmorphism,"Design a glassmorphic interface with frosted glass effect. Use backdrop blur (10-20px), translucent overlays (rgba 10-30% opacity), vibrant background colors, subtle borders, light source reflection, layered depth. Perfect for modern overlays and cards.","backdrop-filter: blur(15px), background: rgba(255, 255, 255, 0.15), border: 1px solid rgba(255,255,255,0.2), -webkit-backdrop-filter: blur(15px), z-index layering for depth","☐ Backdrop-filter blur 10-20px, ☐ Translucent white 15-30% opacity, ☐ Subtle border 1px light, ☐ Vibrant background verified, ☐ Text contrast 4.5:1 checked","--blur-amount: 15px, --glass-opacity: 0.15, --border-color: rgba(255,255,255,0.2), --background: vibrant color, --text-color: light/dark based on BG"
4,Brutalism,"Create a brutalist design with raw, unpolished, stark aesthetic. Use pure primary colors (red, blue, yellow), black & white, no smooth transitions (instant), sharp corners, bold large typography, visible grid lines, default system fonts, intentional 'broken' design elements.","border-radius: 0px, transition: none or 0s, font-family: system-ui or monospace, font-weight: 700+, border: visible 2-4px, colors: #FF0000, #0000FF, #FFFF00, #000000, #FFFFFF","☐ No border-radius (0px), ☐ No transitions (instant), ☐ Bold typography (700+), ☐ Pure primary colors used, ☐ Visible grid/borders, ☐ Asymmetric layout intentional","--border-radius: 0px, --transition-duration: 0s, --font-weight: 700-900, --colors: primary only, --border-style: visible, --grid-visible: true" 4,Brutalism,"Create a brutalist design with raw, unpolished, stark aesthetic. Use pure primary colors (red, blue, yellow), black & white, no smooth transitions (instant), sharp corners, bold large typography, visible grid lines, default system fonts, intentional 'broken' design elements.","border-radius: 0px, transition: none or 0s, font-family: system-ui or monospace, font-weight: 700+, border: visible 2-4px, colors: #FF0000, #0000FF, #FFFF00, #000000, #FFFFFF","☐ No border-radius (0px), ☐ No transitions (instant), ☐ Bold typography (700+), ☐ Pure primary colors used, ☐ Visible grid/borders, ☐ Asymmetric layout intentional","--border-radius: 0px, --transition-duration: 0s, --font-weight: 700-900, --colors: primary only, --border-style: visible, --grid-visible: true"
5,3D & Hyperrealism,"Build an immersive 3D interface using realistic textures, 3D models (Three.js/Babylon.js), complex shadows, realistic lighting, parallax scrolling (3-5 layers), physics-based motion. Include skeuomorphic elements with tactile detail.","transform: translate3d, perspective: 1000px, WebGL canvas, Three.js/Babylon.js library, box-shadow: complex multi-layer, background: complex gradients, filter: drop-shadow()","☐ WebGL/Three.js integrated, ☐ 3D models loaded, ☐ Parallax 3-5 layers, ☐ Realistic lighting verified, ☐ Complex shadows rendered, ☐ Physics animation smooth 300-400ms","--perspective: 1000px, --parallax-layers: 5, --lighting-intensity: realistic, --shadow-depth: 20-40%, --animation-duration: 300-400ms" 5,3D & Hyperrealism,"Build an immersive 3D interface using realistic textures, 3D models (Three.js/Babylon.js), complex shadows, realistic lighting, parallax scrolling (3-5 layers), physics-based motion. Include skeuomorphic elements with tactile detail.","transform: translate3d, perspective: 1000px, WebGL canvas, Three.js/Babylon.js library, box-shadow: complex multi-layer, background: complex gradients, filter: drop-shadow()","☐ WebGL/Three.js integrated, ☐ 3D models loaded, ☐ Parallax 3-5 layers, ☐ Realistic lighting verified, ☐ Complex shadows rendered, ☐ Physics animation smooth 300-400ms","--perspective: 1000px, --parallax-layers: 5, --lighting-intensity: realistic, --shadow-depth: 20-40%, --animation-duration: 300-400ms"
6,Vibrant & Block-based,"Design an energetic, vibrant interface with bold block layouts, geometric shapes, high color contrast, large typography (32px+), animated background patterns, duotone effects. Perfect for startups and youth-focused apps. Use 4-6 contrasting colors from complementary/triadic schemes.","display: flex/grid with large gaps (48px+), font-size: 32px+, background: animated patterns (CSS), color: neon/vibrant colors, animation: continuous pattern movement","☐ Block layout with 48px+ gaps, ☐ Large typography 32px+, ☐ 4-6 vibrant colors max, ☐ Animated patterns active, ☐ Scroll-snap enabled, ☐ High contrast verified (7:1+)","--block-gap: 48px, --typography-size: 32px+, --color-palette: 4-6 vibrant colors, --animation: continuous pattern, --contrast-ratio: 7:1+" 6,Vibrant & Block-based,"Design an energetic, vibrant interface with bold block layouts, geometric shapes, high color contrast, large typography (32px+), animated background patterns, duotone effects. Perfect for startups and youth-focused apps. Use 4-6 contrasting colors from complementary/triadic schemes.","display: flex/grid with large gaps (48px+), font-size: 32px+, background: animated patterns (CSS), color: neon/vibrant colors, animation: continuous pattern movement","☐ Block layout with 48px+ gaps, ☐ Large typography 32px+, ☐ 4-6 vibrant colors max, ☐ Animated patterns active, ☐ Scroll-snap enabled, ☐ High contrast verified (7:1+)","--block-gap: 48px, --typography-size: 32px+, --color-palette: 4-6 vibrant colors, --animation: continuous pattern, --contrast-ratio: 7:1+"
7,Dark Mode (OLED),"Create an OLED-optimized dark interface with deep black (#000000), dark grey (#121212), midnight blue accents. Use minimal glow effects, vibrant neon accents (green, blue, gold, purple), high contrast text. Optimize for eye comfort and OLED power saving.","background: #000000 or #121212, color: #FFFFFF or #E0E0E0, text-shadow: 0 0 10px neon-color (sparingly), filter: brightness(0.8) if needed, color-scheme: dark","☐ Deep black #000000 or #121212, ☐ Vibrant neon accents used, ☐ Text contrast 7:1+, ☐ Minimal glow effects, ☐ OLED power optimization, ☐ No white (#FFFFFF) background","--bg-black: #000000, --bg-dark-grey: #121212, --text-primary: #FFFFFF, --accent-neon: neon colors, --glow-effect: minimal, --oled-optimized: true" 7,Dark Mode (OLED),"Create an OLED-optimized dark interface with deep black (#000000), dark grey (#121212), midnight blue accents. Use minimal glow effects, vibrant neon accents (green, blue, gold, purple), high contrast text. Optimize for eye comfort and OLED power saving.","background: #000000 or #121212, color: #FFFFFF or #E0E0E0, text-shadow: 0 0 10px neon-color (sparingly), filter: brightness(0.8) if needed, color-scheme: dark","☐ Deep black #000000 or #121212, ☐ Vibrant neon accents used, ☐ Text contrast 7:1+, ☐ Minimal glow effects, ☐ OLED power optimization, ☐ No white (#FFFFFF) background","--bg-black: #000000, --bg-dark-grey: #121212, --text-primary: #FFFFFF, --accent-neon: neon colors, --glow-effect: minimal, --oled-optimized: true"
8,Accessible & Ethical,"Design with WCAG AAA compliance. Include: high contrast (7:1+), large text (16px+), keyboard navigation, screen reader compatibility, focus states visible (3-4px ring), semantic HTML, ARIA labels, skip links, reduced motion support (prefers-reduced-motion), 44x44px touch targets.","color-contrast: 7:1+, font-size: 16px+, outline: 3-4px on :focus-visible, aria-label, role attributes, @media (prefers-reduced-motion), touch-target: 44x44px, cursor: pointer","☐ WCAG AAA verified, ☐ 7:1+ contrast checked, ☐ Keyboard navigation tested, ☐ Screen reader tested, ☐ Focus visible 3-4px, ☐ Semantic HTML used, ☐ Touch targets 44x44px","--contrast-ratio: 7:1, --font-size-min: 16px, --focus-ring: 3-4px, --touch-target: 44x44px, --wcag-level: AAA, --keyboard-accessible: true, --sr-tested: true" 8,Accessible & Ethical,"Design with WCAG AAA compliance. Include: high contrast (7:1+), large text (16px+), keyboard navigation, screen reader compatibility, focus states visible (3-4px ring), semantic HTML, ARIA labels, skip links, reduced motion support (prefers-reduced-motion), 44x44px touch targets.","color-contrast: 7:1+, font-size: 16px+, outline: 3-4px on :focus-visible, aria-label, role attributes, @media (prefers-reduced-motion), touch-target: 44x44px, cursor: pointer","☐ WCAG AAA verified, ☐ 7:1+ contrast checked, ☐ Keyboard navigation tested, ☐ Screen reader tested, ☐ Focus visible 3-4px, ☐ Semantic HTML used, ☐ Touch targets 44x44px","--contrast-ratio: 7:1, --font-size-min: 16px, --focus-ring: 3-4px, --touch-target: 44x44px, --wcag-level: AAA, --keyboard-accessible: true, --sr-tested: true"
9,Claymorphism,"Design a playful, toy-like interface with soft 3D, chunky elements, bubbly aesthetic, rounded edges (16-24px), thick borders (3-4px), double shadows (inner + outer), pastel colors, smooth animations. Perfect for children's apps and creative tools.","border-radius: 16-24px, border: 3-4px solid, box-shadow: inset -2px -2px 8px, 4px 4px 8px, background: pastel-gradient, animation: soft bounce (cubic-bezier 0.34, 1.56)","☐ Border-radius 16-24px, ☐ Thick borders 3-4px, ☐ Double shadows (inner+outer), ☐ Pastel colors used, ☐ Soft bounce animations, ☐ Playful interactions","--border-radius: 20px, --border-width: 3-4px, --shadow-inner: inset -2px -2px 8px, --shadow-outer: 4px 4px 8px, --color-palette: pastels, --animation: bounce" 9,Claymorphism,"Design a playful, toy-like interface with soft 3D, chunky elements, bubbly aesthetic, rounded edges (16-24px), thick borders (3-4px), double shadows (inner + outer), pastel colors, smooth animations. Perfect for children's apps and creative tools.","border-radius: 16-24px, border: 3-4px solid, box-shadow: inset -2px -2px 8px, 4px 4px 8px, background: pastel-gradient, animation: soft bounce (cubic-bezier 0.34, 1.56)","☐ Border-radius 16-24px, ☐ Thick borders 3-4px, ☐ Double shadows (inner+outer), ☐ Pastel colors used, ☐ Soft bounce animations, ☐ Playful interactions","--border-radius: 20px, --border-width: 3-4px, --shadow-inner: inset -2px -2px 8px, --shadow-outer: 4px 4px 8px, --color-palette: pastels, --animation: bounce"
10,Aurora UI,"Create a vibrant gradient interface inspired by Northern Lights with mesh gradients, smooth color blends, flowing animations. Use complementary color pairs (blue-orange, purple-yellow), flowing background gradients, subtle continuous animations (8-12s loops), iridescent effects.","background: conic-gradient or radial-gradient with multiple stops, animation: @keyframes gradient (8-12s), background-size: 200% 200%, filter: saturate(1.2), blend-mode: screen or multiply","☐ Mesh/flowing gradients applied, ☐ 8-12s animation loop, ☐ Complementary colors used, ☐ Smooth color transitions, ☐ Iridescent effect subtle, ☐ Text contrast verified","--gradient-colors: complementary pairs, --animation-duration: 8-12s, --blend-mode: screen, --color-saturation: 1.2, --effect: iridescent, --loop-smooth: true" 10,Aurora UI,"Create a vibrant gradient interface inspired by Northern Lights with mesh gradients, smooth color blends, flowing animations. Use complementary color pairs (blue-orange, purple-yellow), flowing background gradients, subtle continuous animations (8-12s loops), iridescent effects.","background: conic-gradient or radial-gradient with multiple stops, animation: @keyframes gradient (8-12s), background-size: 200% 200%, filter: saturate(1.2), blend-mode: screen or multiply","☐ Mesh/flowing gradients applied, ☐ 8-12s animation loop, ☐ Complementary colors used, ☐ Smooth color transitions, ☐ Iridescent effect subtle, ☐ Text contrast verified","--gradient-colors: complementary pairs, --animation-duration: 8-12s, --blend-mode: screen, --color-saturation: 1.2, --effect: iridescent, --loop-smooth: true"
11,Retro-Futurism,"Build a retro-futuristic (cyberpunk/vaporwave) interface with neon colors (blue, pink, cyan), deep black background, 80s aesthetic, CRT scanlines, glitch effects, neon glow text/borders, monospace fonts, geometric patterns. Use neon text-shadow and animated glitch effects.","color: neon colors (#0080FF, #FF006E, #00FFFF), text-shadow: 0 0 10px neon, background: #000 or #1A1A2E, font-family: monospace, animation: glitch (skew+offset), filter: hue-rotate","☐ Neon colors used, ☐ CRT scanlines effect, ☐ Glitch animations active, ☐ Monospace font, ☐ Deep black background, ☐ Glow effects applied, ☐ 80s patterns present","--neon-colors: #0080FF #FF006E #00FFFF, --background: #000000, --font-family: monospace, --effect: glitch+glow, --scanline-opacity: 0.3, --crt-effect: true" 11,Retro-Futurism,"Build a retro-futuristic (cyberpunk/vaporwave) interface with neon colors (blue, pink, cyan), deep black background, 80s aesthetic, CRT scanlines, glitch effects, neon glow text/borders, monospace fonts, geometric patterns. Use neon text-shadow and animated glitch effects.","color: neon colors (#0080FF, #FF006E, #00FFFF), text-shadow: 0 0 10px neon, background: #000 or #1A1A2E, font-family: monospace, animation: glitch (skew+offset), filter: hue-rotate","☐ Neon colors used, ☐ CRT scanlines effect, ☐ Glitch animations active, ☐ Monospace font, ☐ Deep black background, ☐ Glow effects applied, ☐ 80s patterns present","--neon-colors: #0080FF #FF006E #00FFFF, --background: #000000, --font-family: monospace, --effect: glitch+glow, --scanline-opacity: 0.3, --crt-effect: true"
12,Flat Design,"Create a flat, 2D interface with bold colors, no shadows/gradients, clean lines, simple geometric shapes, icon-heavy, typography-focused, minimal ornamentation. Use 4-6 solid, bright colors in a limited palette with high saturation.","box-shadow: none, background: solid color, border-radius: 0-4px, color: solid (no gradients), fill: solid, stroke: 1-2px, font: bold sans-serif, icons: simplified SVG","☐ No shadows/gradients, ☐ 4-6 solid colors max, ☐ Clean lines consistent, ☐ Simple shapes used, ☐ Icon-heavy layout, ☐ High saturation colors, ☐ Fast loading verified","--shadow: none, --color-palette: 4-6 solid, --border-radius: 2px, --gradient: none, --icons: simplified SVG, --animation: minimal 150-200ms" 12,Flat Design,"Create a flat, 2D interface with bold colors, no shadows/gradients, clean lines, simple geometric shapes, icon-heavy, typography-focused, minimal ornamentation. Use 4-6 solid, bright colors in a limited palette with high saturation.","box-shadow: none, background: solid color, border-radius: 0-4px, color: solid (no gradients), fill: solid, stroke: 1-2px, font: bold sans-serif, icons: simplified SVG","☐ No shadows/gradients, ☐ 4-6 solid colors max, ☐ Clean lines consistent, ☐ Simple shapes used, ☐ Icon-heavy layout, ☐ High saturation colors, ☐ Fast loading verified","--shadow: none, --color-palette: 4-6 solid, --border-radius: 2px, --gradient: none, --icons: simplified SVG, --animation: minimal 150-200ms"
13,Skeuomorphism,"Design a realistic, textured interface with 3D depth, real-world metaphors (leather, wood, metal), complex gradients (8-12 stops), realistic shadows, grain/texture overlays, tactile press animations. Perfect for premium/luxury products.","background: complex gradient (8-12 stops), box-shadow: realistic multi-layer, background-image: texture overlay (noise, grain), filter: drop-shadow, transform: scale on press (300-500ms)","☐ Realistic textures applied, ☐ Complex gradients 8-12 stops, ☐ Multi-layer shadows, ☐ Texture overlays present, ☐ Tactile animations smooth, ☐ Depth effect pronounced","--gradient-stops: 8-12, --texture-overlay: noise+grain, --shadow-layers: 3+, --animation-duration: 300-500ms, --depth-effect: pronounced, --tactile: true" 13,Skeuomorphism,"Design a realistic, textured interface with 3D depth, real-world metaphors (leather, wood, metal), complex gradients (8-12 stops), realistic shadows, grain/texture overlays, tactile press animations. Perfect for premium/luxury products.","background: complex gradient (8-12 stops), box-shadow: realistic multi-layer, background-image: texture overlay (noise, grain), filter: drop-shadow, transform: scale on press (300-500ms)","☐ Realistic textures applied, ☐ Complex gradients 8-12 stops, ☐ Multi-layer shadows, ☐ Texture overlays present, ☐ Tactile animations smooth, ☐ Depth effect pronounced","--gradient-stops: 8-12, --texture-overlay: noise+grain, --shadow-layers: 3+, --animation-duration: 300-500ms, --depth-effect: pronounced, --tactile: true"
14,Liquid Glass,"Create a premium liquid glass effect with morphing shapes, flowing animations, chromatic aberration, iridescent gradients, smooth 400-600ms transitions. Use SVG morphing for shape changes, dynamic blur, smooth color transitions creating a fluid, premium feel.","animation: morphing SVG paths (400-600ms), backdrop-filter: blur + saturate, filter: hue-rotate + brightness, blend-mode: screen, background: iridescent gradient","☐ Morphing animations 400-600ms, ☐ Chromatic aberration applied, ☐ Dynamic blur active, ☐ Iridescent gradients, ☐ Smooth color transitions, ☐ Premium feel achieved","--morph-duration: 400-600ms, --blur-amount: 15px, --chromatic-aberration: true, --iridescent: true, --blend-mode: screen, --smooth-transitions: true" 14,Liquid Glass,"Create a premium liquid glass effect with morphing shapes, flowing animations, chromatic aberration, iridescent gradients, smooth 400-600ms transitions. Use SVG morphing for shape changes, dynamic blur, smooth color transitions creating a fluid, premium feel.","animation: morphing SVG paths (400-600ms), backdrop-filter: blur + saturate, filter: hue-rotate + brightness, blend-mode: screen, background: iridescent gradient","☐ Morphing animations 400-600ms, ☐ Chromatic aberration applied, ☐ Dynamic blur active, ☐ Iridescent gradients, ☐ Smooth color transitions, ☐ Premium feel achieved","--morph-duration: 400-600ms, --blur-amount: 15px, --chromatic-aberration: true, --iridescent: true, --blend-mode: screen, --smooth-transitions: true"
15,Motion-Driven,"Build an animation-heavy interface with scroll-triggered animations, microinteractions, parallax scrolling (3-5 layers), smooth transitions (300-400ms), entrance animations, page transitions. Use Intersection Observer for scroll effects, transform for performance, GPU acceleration.","animation: @keyframes scroll-reveal, transform: translateY/X, Intersection Observer API, will-change: transform, scroll-behavior: smooth, animation-duration: 300-400ms","☐ Scroll animations active, ☐ Parallax 3-5 layers, ☐ Entrance animations smooth, ☐ Page transitions fluid, ☐ GPU accelerated, ☐ Prefers-reduced-motion respected","--animation-duration: 300-400ms, --parallax-layers: 5, --scroll-behavior: smooth, --gpu-accelerated: true, --entrance-animation: true, --page-transition: smooth" 15,Motion-Driven,"Build an animation-heavy interface with scroll-triggered animations, microinteractions, parallax scrolling (3-5 layers), smooth transitions (300-400ms), entrance animations, page transitions. Use Intersection Observer for scroll effects, transform for performance, GPU acceleration.","animation: @keyframes scroll-reveal, transform: translateY/X, Intersection Observer API, will-change: transform, scroll-behavior: smooth, animation-duration: 300-400ms","☐ Scroll animations active, ☐ Parallax 3-5 layers, ☐ Entrance animations smooth, ☐ Page transitions fluid, ☐ GPU accelerated, ☐ Prefers-reduced-motion respected","--animation-duration: 300-400ms, --parallax-layers: 5, --scroll-behavior: smooth, --gpu-accelerated: true, --entrance-animation: true, --page-transition: smooth"
16,Micro-interactions,"Design with delightful micro-interactions: small 50-100ms animations, gesture-based responses, tactile feedback, loading spinners, success/error states, subtle hover effects, haptic feedback triggers for mobile. Focus on responsive, contextual interactions.","animation: short 50-100ms, transition: hover states, @media (hover: hover) for desktop, :active for press, haptic-feedback CSS/API, loading animation smooth loop","☐ Micro-animations 50-100ms, ☐ Gesture-responsive, ☐ Tactile feedback visual/haptic, ☐ Loading spinners smooth, ☐ Success/error states clear, ☐ Hover effects subtle","--micro-animation-duration: 50-100ms, --gesture-responsive: true, --haptic-feedback: true, --loading-animation: smooth, --state-feedback: success+error" 16,Micro-interactions,"Design with delightful micro-interactions: small 50-100ms animations, gesture-based responses, tactile feedback, loading spinners, success/error states, subtle hover effects, haptic feedback triggers for mobile. Focus on responsive, contextual interactions.","animation: short 50-100ms, transition: hover states, @media (hover: hover) for desktop, :active for press, haptic-feedback CSS/API, loading animation smooth loop","☐ Micro-animations 50-100ms, ☐ Gesture-responsive, ☐ Tactile feedback visual/haptic, ☐ Loading spinners smooth, ☐ Success/error states clear, ☐ Hover effects subtle","--micro-animation-duration: 50-100ms, --gesture-responsive: true, --haptic-feedback: true, --loading-animation: smooth, --state-feedback: success+error"
17,Inclusive Design,"Design for universal accessibility: high contrast (7:1+), large text (16px+), keyboard-only navigation, screen reader optimization, WCAG AAA compliance, symbol-based color indicators (not color-only), haptic feedback, voice interaction support, reduced motion options.","aria-* attributes complete, role attributes semantic, focus-visible: 3-4px ring, color-contrast: 7:1+, @media (prefers-reduced-motion), alt text on all images, form labels properly associated","☐ WCAG AAA verified, ☐ 7:1+ contrast all text, ☐ Keyboard accessible (Tab/Enter), ☐ Screen reader tested, ☐ Focus visible 3-4px, ☐ No color-only indicators, ☐ Haptic fallback","--contrast-ratio: 7:1, --font-size: 16px+, --keyboard-accessible: true, --sr-compatible: true, --wcag-level: AAA, --color-symbols: true, --haptic: enabled" 17,Inclusive Design,"Design for universal accessibility: high contrast (7:1+), large text (16px+), keyboard-only navigation, screen reader optimization, WCAG AAA compliance, symbol-based color indicators (not color-only), haptic feedback, voice interaction support, reduced motion options.","aria-* attributes complete, role attributes semantic, focus-visible: 3-4px ring, color-contrast: 7:1+, @media (prefers-reduced-motion), alt text on all images, form labels properly associated","☐ WCAG AAA verified, ☐ 7:1+ contrast all text, ☐ Keyboard accessible (Tab/Enter), ☐ Screen reader tested, ☐ Focus visible 3-4px, ☐ No color-only indicators, ☐ Haptic fallback","--contrast-ratio: 7:1, --font-size: 16px+, --keyboard-accessible: true, --sr-compatible: true, --wcag-level: AAA, --color-symbols: true, --haptic: enabled"
18,Zero Interface,"Create a voice-first, gesture-based, AI-driven interface with minimal visible UI, progressive disclosure, voice recognition UI, gesture detection, AI predictions, smart suggestions, context-aware actions. Hide controls until needed.","voice-commands: Web Speech API, gesture-detection: touch events, AI-predictions: hidden by default (reveal on hover), progressive-disclosure: show on demand, minimal UI visible","☐ Voice commands responsive, ☐ Gesture detection active, ☐ AI predictions hidden/revealed, ☐ Progressive disclosure working, ☐ Minimal visible UI, ☐ Smart suggestions contextual","--voice-ui: enabled, --gesture-detection: active, --ai-predictions: smart, --progressive-disclosure: true, --visible-ui: minimal, --context-aware: true" 18,Zero Interface,"Create a voice-first, gesture-based, AI-driven interface with minimal visible UI, progressive disclosure, voice recognition UI, gesture detection, AI predictions, smart suggestions, context-aware actions. Hide controls until needed.","voice-commands: Web Speech API, gesture-detection: touch events, AI-predictions: hidden by default (reveal on hover), progressive-disclosure: show on demand, minimal UI visible","☐ Voice commands responsive, ☐ Gesture detection active, ☐ AI predictions hidden/revealed, ☐ Progressive disclosure working, ☐ Minimal visible UI, ☐ Smart suggestions contextual","--voice-ui: enabled, --gesture-detection: active, --ai-predictions: smart, --progressive-disclosure: true, --visible-ui: minimal, --context-aware: true"
19,Soft UI Evolution,"Design evolved neumorphism with improved contrast (WCAG AA+), modern aesthetics, subtle depth, accessibility focus. Use soft shadows (softer than flat but clearer than pure neumorphism), better color hierarchy, improved focus states, modern 200-300ms animations.","box-shadow: softer multi-layer (0 2px 4px), background: improved contrast pastels, border-radius: 8-12px, animation: 200-300ms smooth, outline: 2-3px on focus, contrast: 4.5:1+","☐ Improved contrast AA/AAA, ☐ Soft shadows modern, ☐ Border-radius 8-12px, ☐ Animations 200-300ms, ☐ Focus states visible, ☐ Color hierarchy clear","--shadow-soft: modern blend, --border-radius: 10px, --animation-duration: 200-300ms, --contrast-ratio: 4.5:1+, --color-hierarchy: improved, --wcag-level: AA+" 19,Soft UI Evolution,"Design evolved neumorphism with improved contrast (WCAG AA+), modern aesthetics, subtle depth, accessibility focus. Use soft shadows (softer than flat but clearer than pure neumorphism), better color hierarchy, improved focus states, modern 200-300ms animations.","box-shadow: softer multi-layer (0 2px 4px), background: improved contrast pastels, border-radius: 8-12px, animation: 200-300ms smooth, outline: 2-3px on focus, contrast: 4.5:1+","☐ Improved contrast AA/AAA, ☐ Soft shadows modern, ☐ Border-radius 8-12px, ☐ Animations 200-300ms, ☐ Focus states visible, ☐ Color hierarchy clear","--shadow-soft: modern blend, --border-radius: 10px, --animation-duration: 200-300ms, --contrast-ratio: 4.5:1+, --color-hierarchy: improved, --wcag-level: AA+"
20,Bento Grids,"Design a Bento Grid layout. Use: modular grid system, rounded corners (16-24px), different card sizes (1x1, 2x1, 2x2), card-based hierarchy, soft backgrounds (#F5F5F7), subtle borders, content-first, Apple-style aesthetic.","display: grid, grid-template-columns: repeat(auto-fit, minmax(...)), gap: 1rem, border-radius: 20px, background: #FFF, box-shadow: subtle","☐ Grid layout (CSS Grid), ☐ Rounded corners 16-24px, ☐ Varied card spans, ☐ Content fits card size, ☐ Responsive re-flow, ☐ Apple-like aesthetic","--grid-gap: 20px, --card-radius: 24px, --card-bg: #FFFFFF, --page-bg: #F5F5F7, --shadow: soft" 20,Bento Grids,"Design a Bento Grid layout. Use: modular grid system, rounded corners (16-24px), different card sizes (1x1, 2x1, 2x2), card-based hierarchy, soft backgrounds (#F5F5F7), subtle borders, content-first, Apple-style aesthetic.","display: grid, grid-template-columns: repeat(auto-fit, minmax(...)), gap: 1rem, border-radius: 20px, background: #FFF, box-shadow: subtle","☐ Grid layout (CSS Grid), ☐ Rounded corners 16-24px, ☐ Varied card spans, ☐ Content fits card size, ☐ Responsive re-flow, ☐ Apple-like aesthetic","--grid-gap: 20px, --card-radius: 24px, --card-bg: #FFFFFF, --page-bg: #F5F5F7, --shadow: soft"
21,Neubrutalism,"Design a neubrutalist interface. Use: high contrast, hard black borders (3px+), bright pop colors, no blur, sharp or slightly rounded corners, bold typography, hard shadows (offset 4px 4px), raw aesthetic but functional.","border: 3px solid black, box-shadow: 5px 5px 0px black, colors: #FFDB58 #FF6B6B #4ECDC4, font-weight: 700, no gradients","☐ Hard borders (2-4px), ☐ Hard offset shadows, ☐ High saturation colors, ☐ Bold typography, ☐ No blurs/gradients, ☐ Distinctive 'ugly-cute' look","--border-width: 3px, --shadow-offset: 4px, --shadow-color: #000, --colors: high saturation, --font: bold sans" 21,Neubrutalism,"Design a neubrutalist interface. Use: high contrast, hard black borders (3px+), bright pop colors, no blur, sharp or slightly rounded corners, bold typography, hard shadows (offset 4px 4px), raw aesthetic but functional.","border: 3px solid black, box-shadow: 5px 5px 0px black, colors: #FFDB58 #FF6B6B #4ECDC4, font-weight: 700, no gradients","☐ Hard borders (2-4px), ☐ Hard offset shadows, ☐ High saturation colors, ☐ Bold typography, ☐ No blurs/gradients, ☐ Distinctive 'ugly-cute' look","--border-width: 3px, --shadow-offset: 4px, --shadow-color: #000, --colors: high saturation, --font: bold sans"
22,HUD / Sci-Fi FUI,"Design a futuristic HUD (Heads Up Display) or FUI. Use: thin lines (1px), neon cyan/blue on black, technical markers, decorative brackets, data visualization, monospaced tech fonts, glowing elements, transparency.","border: 1px solid rgba(0,255,255,0.5), color: #00FFFF, background: transparent or rgba(0,0,0,0.8), font-family: monospace, text-shadow: 0 0 5px cyan","☐ Fine lines 1px, ☐ Neon glow text/borders, ☐ Monospaced font, ☐ Dark/Transparent BG, ☐ Decorative tech markers, ☐ Holographic feel","--hud-color: #00FFFF, --bg-color: rgba(0,10,20,0.9), --line-width: 1px, --glow: 0 0 5px, --font: monospace" 22,HUD / Sci-Fi FUI,"Design a futuristic HUD (Heads Up Display) or FUI. Use: thin lines (1px), neon cyan/blue on black, technical markers, decorative brackets, data visualization, monospaced tech fonts, glowing elements, transparency.","border: 1px solid rgba(0,255,255,0.5), color: #00FFFF, background: transparent or rgba(0,0,0,0.8), font-family: monospace, text-shadow: 0 0 5px cyan","☐ Fine lines 1px, ☐ Neon glow text/borders, ☐ Monospaced font, ☐ Dark/Transparent BG, ☐ Decorative tech markers, ☐ Holographic feel","--hud-color: #00FFFF, --bg-color: rgba(0,10,20,0.9), --line-width: 1px, --glow: 0 0 5px, --font: monospace"
1 STT Style Category AI Prompt Keywords (Copy-Paste Ready) CSS/Technical Keywords Implementation Checklist Design System Variables
2 1 Minimalism & Swiss Style Design a minimalist landing page. Use: white space, geometric layouts, sans-serif fonts, high contrast, grid-based structure, essential elements only. Avoid shadows and gradients. Focus on clarity and functionality. display: grid, gap: 2rem, font-family: sans-serif, color: #000 or #FFF, max-width: 1200px, clean borders, no box-shadow unless necessary ☐ Grid-based layout 12-16 columns, ☐ Typography hierarchy clear, ☐ No unnecessary decorations, ☐ WCAG AAA contrast verified, ☐ Mobile responsive grid --spacing: 2rem, --border-radius: 0px, --font-weight: 400-700, --shadow: none, --accent-color: single primary only
3 2 Neumorphism Create a neumorphic UI with soft 3D effects. Use light pastels, rounded corners (12-16px), subtle soft shadows (multiple layers), no hard lines, monochromatic color scheme with light/dark variations. Embossed/debossed effect on interactive elements. border-radius: 12-16px, box-shadow: -5px -5px 15px rgba(0,0,0,0.1), 5px 5px 15px rgba(255,255,255,0.8), background: linear-gradient(145deg, color1, color2), transform: scale on press ☐ Rounded corners 12-16px consistent, ☐ Multiple shadow layers (2-3), ☐ Pastel color verified, ☐ Monochromatic palette checked, ☐ Press animation smooth 150ms --border-radius: 14px, --shadow-soft-1: -5px -5px 15px, --shadow-soft-2: 5px 5px 15px, --color-light: #F5F5F5, --color-primary: single pastel
4 3 Glassmorphism Design a glassmorphic interface with frosted glass effect. Use backdrop blur (10-20px), translucent overlays (rgba 10-30% opacity), vibrant background colors, subtle borders, light source reflection, layered depth. Perfect for modern overlays and cards. backdrop-filter: blur(15px), background: rgba(255, 255, 255, 0.15), border: 1px solid rgba(255,255,255,0.2), -webkit-backdrop-filter: blur(15px), z-index layering for depth ☐ Backdrop-filter blur 10-20px, ☐ Translucent white 15-30% opacity, ☐ Subtle border 1px light, ☐ Vibrant background verified, ☐ Text contrast 4.5:1 checked --blur-amount: 15px, --glass-opacity: 0.15, --border-color: rgba(255,255,255,0.2), --background: vibrant color, --text-color: light/dark based on BG
5 4 Brutalism Create a brutalist design with raw, unpolished, stark aesthetic. Use pure primary colors (red, blue, yellow), black & white, no smooth transitions (instant), sharp corners, bold large typography, visible grid lines, default system fonts, intentional 'broken' design elements. border-radius: 0px, transition: none or 0s, font-family: system-ui or monospace, font-weight: 700+, border: visible 2-4px, colors: #FF0000, #0000FF, #FFFF00, #000000, #FFFFFF ☐ No border-radius (0px), ☐ No transitions (instant), ☐ Bold typography (700+), ☐ Pure primary colors used, ☐ Visible grid/borders, ☐ Asymmetric layout intentional --border-radius: 0px, --transition-duration: 0s, --font-weight: 700-900, --colors: primary only, --border-style: visible, --grid-visible: true
6 5 3D & Hyperrealism Build an immersive 3D interface using realistic textures, 3D models (Three.js/Babylon.js), complex shadows, realistic lighting, parallax scrolling (3-5 layers), physics-based motion. Include skeuomorphic elements with tactile detail. transform: translate3d, perspective: 1000px, WebGL canvas, Three.js/Babylon.js library, box-shadow: complex multi-layer, background: complex gradients, filter: drop-shadow() ☐ WebGL/Three.js integrated, ☐ 3D models loaded, ☐ Parallax 3-5 layers, ☐ Realistic lighting verified, ☐ Complex shadows rendered, ☐ Physics animation smooth 300-400ms --perspective: 1000px, --parallax-layers: 5, --lighting-intensity: realistic, --shadow-depth: 20-40%, --animation-duration: 300-400ms
7 6 Vibrant & Block-based Design an energetic, vibrant interface with bold block layouts, geometric shapes, high color contrast, large typography (32px+), animated background patterns, duotone effects. Perfect for startups and youth-focused apps. Use 4-6 contrasting colors from complementary/triadic schemes. display: flex/grid with large gaps (48px+), font-size: 32px+, background: animated patterns (CSS), color: neon/vibrant colors, animation: continuous pattern movement ☐ Block layout with 48px+ gaps, ☐ Large typography 32px+, ☐ 4-6 vibrant colors max, ☐ Animated patterns active, ☐ Scroll-snap enabled, ☐ High contrast verified (7:1+) --block-gap: 48px, --typography-size: 32px+, --color-palette: 4-6 vibrant colors, --animation: continuous pattern, --contrast-ratio: 7:1+
8 7 Dark Mode (OLED) Create an OLED-optimized dark interface with deep black (#000000), dark grey (#121212), midnight blue accents. Use minimal glow effects, vibrant neon accents (green, blue, gold, purple), high contrast text. Optimize for eye comfort and OLED power saving. background: #000000 or #121212, color: #FFFFFF or #E0E0E0, text-shadow: 0 0 10px neon-color (sparingly), filter: brightness(0.8) if needed, color-scheme: dark ☐ Deep black #000000 or #121212, ☐ Vibrant neon accents used, ☐ Text contrast 7:1+, ☐ Minimal glow effects, ☐ OLED power optimization, ☐ No white (#FFFFFF) background --bg-black: #000000, --bg-dark-grey: #121212, --text-primary: #FFFFFF, --accent-neon: neon colors, --glow-effect: minimal, --oled-optimized: true
9 8 Accessible & Ethical Design with WCAG AAA compliance. Include: high contrast (7:1+), large text (16px+), keyboard navigation, screen reader compatibility, focus states visible (3-4px ring), semantic HTML, ARIA labels, skip links, reduced motion support (prefers-reduced-motion), 44x44px touch targets. color-contrast: 7:1+, font-size: 16px+, outline: 3-4px on :focus-visible, aria-label, role attributes, @media (prefers-reduced-motion), touch-target: 44x44px, cursor: pointer ☐ WCAG AAA verified, ☐ 7:1+ contrast checked, ☐ Keyboard navigation tested, ☐ Screen reader tested, ☐ Focus visible 3-4px, ☐ Semantic HTML used, ☐ Touch targets 44x44px --contrast-ratio: 7:1, --font-size-min: 16px, --focus-ring: 3-4px, --touch-target: 44x44px, --wcag-level: AAA, --keyboard-accessible: true, --sr-tested: true
10 9 Claymorphism Design a playful, toy-like interface with soft 3D, chunky elements, bubbly aesthetic, rounded edges (16-24px), thick borders (3-4px), double shadows (inner + outer), pastel colors, smooth animations. Perfect for children's apps and creative tools. border-radius: 16-24px, border: 3-4px solid, box-shadow: inset -2px -2px 8px, 4px 4px 8px, background: pastel-gradient, animation: soft bounce (cubic-bezier 0.34, 1.56) ☐ Border-radius 16-24px, ☐ Thick borders 3-4px, ☐ Double shadows (inner+outer), ☐ Pastel colors used, ☐ Soft bounce animations, ☐ Playful interactions --border-radius: 20px, --border-width: 3-4px, --shadow-inner: inset -2px -2px 8px, --shadow-outer: 4px 4px 8px, --color-palette: pastels, --animation: bounce
11 10 Aurora UI Create a vibrant gradient interface inspired by Northern Lights with mesh gradients, smooth color blends, flowing animations. Use complementary color pairs (blue-orange, purple-yellow), flowing background gradients, subtle continuous animations (8-12s loops), iridescent effects. background: conic-gradient or radial-gradient with multiple stops, animation: @keyframes gradient (8-12s), background-size: 200% 200%, filter: saturate(1.2), blend-mode: screen or multiply ☐ Mesh/flowing gradients applied, ☐ 8-12s animation loop, ☐ Complementary colors used, ☐ Smooth color transitions, ☐ Iridescent effect subtle, ☐ Text contrast verified --gradient-colors: complementary pairs, --animation-duration: 8-12s, --blend-mode: screen, --color-saturation: 1.2, --effect: iridescent, --loop-smooth: true
12 11 Retro-Futurism Build a retro-futuristic (cyberpunk/vaporwave) interface with neon colors (blue, pink, cyan), deep black background, 80s aesthetic, CRT scanlines, glitch effects, neon glow text/borders, monospace fonts, geometric patterns. Use neon text-shadow and animated glitch effects. color: neon colors (#0080FF, #FF006E, #00FFFF), text-shadow: 0 0 10px neon, background: #000 or #1A1A2E, font-family: monospace, animation: glitch (skew+offset), filter: hue-rotate ☐ Neon colors used, ☐ CRT scanlines effect, ☐ Glitch animations active, ☐ Monospace font, ☐ Deep black background, ☐ Glow effects applied, ☐ 80s patterns present --neon-colors: #0080FF #FF006E #00FFFF, --background: #000000, --font-family: monospace, --effect: glitch+glow, --scanline-opacity: 0.3, --crt-effect: true
13 12 Flat Design Create a flat, 2D interface with bold colors, no shadows/gradients, clean lines, simple geometric shapes, icon-heavy, typography-focused, minimal ornamentation. Use 4-6 solid, bright colors in a limited palette with high saturation. box-shadow: none, background: solid color, border-radius: 0-4px, color: solid (no gradients), fill: solid, stroke: 1-2px, font: bold sans-serif, icons: simplified SVG ☐ No shadows/gradients, ☐ 4-6 solid colors max, ☐ Clean lines consistent, ☐ Simple shapes used, ☐ Icon-heavy layout, ☐ High saturation colors, ☐ Fast loading verified --shadow: none, --color-palette: 4-6 solid, --border-radius: 2px, --gradient: none, --icons: simplified SVG, --animation: minimal 150-200ms
14 13 Skeuomorphism Design a realistic, textured interface with 3D depth, real-world metaphors (leather, wood, metal), complex gradients (8-12 stops), realistic shadows, grain/texture overlays, tactile press animations. Perfect for premium/luxury products. background: complex gradient (8-12 stops), box-shadow: realistic multi-layer, background-image: texture overlay (noise, grain), filter: drop-shadow, transform: scale on press (300-500ms) ☐ Realistic textures applied, ☐ Complex gradients 8-12 stops, ☐ Multi-layer shadows, ☐ Texture overlays present, ☐ Tactile animations smooth, ☐ Depth effect pronounced --gradient-stops: 8-12, --texture-overlay: noise+grain, --shadow-layers: 3+, --animation-duration: 300-500ms, --depth-effect: pronounced, --tactile: true
15 14 Liquid Glass Create a premium liquid glass effect with morphing shapes, flowing animations, chromatic aberration, iridescent gradients, smooth 400-600ms transitions. Use SVG morphing for shape changes, dynamic blur, smooth color transitions creating a fluid, premium feel. animation: morphing SVG paths (400-600ms), backdrop-filter: blur + saturate, filter: hue-rotate + brightness, blend-mode: screen, background: iridescent gradient ☐ Morphing animations 400-600ms, ☐ Chromatic aberration applied, ☐ Dynamic blur active, ☐ Iridescent gradients, ☐ Smooth color transitions, ☐ Premium feel achieved --morph-duration: 400-600ms, --blur-amount: 15px, --chromatic-aberration: true, --iridescent: true, --blend-mode: screen, --smooth-transitions: true
16 15 Motion-Driven Build an animation-heavy interface with scroll-triggered animations, microinteractions, parallax scrolling (3-5 layers), smooth transitions (300-400ms), entrance animations, page transitions. Use Intersection Observer for scroll effects, transform for performance, GPU acceleration. animation: @keyframes scroll-reveal, transform: translateY/X, Intersection Observer API, will-change: transform, scroll-behavior: smooth, animation-duration: 300-400ms ☐ Scroll animations active, ☐ Parallax 3-5 layers, ☐ Entrance animations smooth, ☐ Page transitions fluid, ☐ GPU accelerated, ☐ Prefers-reduced-motion respected --animation-duration: 300-400ms, --parallax-layers: 5, --scroll-behavior: smooth, --gpu-accelerated: true, --entrance-animation: true, --page-transition: smooth
17 16 Micro-interactions Design with delightful micro-interactions: small 50-100ms animations, gesture-based responses, tactile feedback, loading spinners, success/error states, subtle hover effects, haptic feedback triggers for mobile. Focus on responsive, contextual interactions. animation: short 50-100ms, transition: hover states, @media (hover: hover) for desktop, :active for press, haptic-feedback CSS/API, loading animation smooth loop ☐ Micro-animations 50-100ms, ☐ Gesture-responsive, ☐ Tactile feedback visual/haptic, ☐ Loading spinners smooth, ☐ Success/error states clear, ☐ Hover effects subtle --micro-animation-duration: 50-100ms, --gesture-responsive: true, --haptic-feedback: true, --loading-animation: smooth, --state-feedback: success+error
18 17 Inclusive Design Design for universal accessibility: high contrast (7:1+), large text (16px+), keyboard-only navigation, screen reader optimization, WCAG AAA compliance, symbol-based color indicators (not color-only), haptic feedback, voice interaction support, reduced motion options. aria-* attributes complete, role attributes semantic, focus-visible: 3-4px ring, color-contrast: 7:1+, @media (prefers-reduced-motion), alt text on all images, form labels properly associated ☐ WCAG AAA verified, ☐ 7:1+ contrast all text, ☐ Keyboard accessible (Tab/Enter), ☐ Screen reader tested, ☐ Focus visible 3-4px, ☐ No color-only indicators, ☐ Haptic fallback --contrast-ratio: 7:1, --font-size: 16px+, --keyboard-accessible: true, --sr-compatible: true, --wcag-level: AAA, --color-symbols: true, --haptic: enabled
19 18 Zero Interface Create a voice-first, gesture-based, AI-driven interface with minimal visible UI, progressive disclosure, voice recognition UI, gesture detection, AI predictions, smart suggestions, context-aware actions. Hide controls until needed. voice-commands: Web Speech API, gesture-detection: touch events, AI-predictions: hidden by default (reveal on hover), progressive-disclosure: show on demand, minimal UI visible ☐ Voice commands responsive, ☐ Gesture detection active, ☐ AI predictions hidden/revealed, ☐ Progressive disclosure working, ☐ Minimal visible UI, ☐ Smart suggestions contextual --voice-ui: enabled, --gesture-detection: active, --ai-predictions: smart, --progressive-disclosure: true, --visible-ui: minimal, --context-aware: true
20 19 Soft UI Evolution Design evolved neumorphism with improved contrast (WCAG AA+), modern aesthetics, subtle depth, accessibility focus. Use soft shadows (softer than flat but clearer than pure neumorphism), better color hierarchy, improved focus states, modern 200-300ms animations. box-shadow: softer multi-layer (0 2px 4px), background: improved contrast pastels, border-radius: 8-12px, animation: 200-300ms smooth, outline: 2-3px on focus, contrast: 4.5:1+ ☐ Improved contrast AA/AAA, ☐ Soft shadows modern, ☐ Border-radius 8-12px, ☐ Animations 200-300ms, ☐ Focus states visible, ☐ Color hierarchy clear --shadow-soft: modern blend, --border-radius: 10px, --animation-duration: 200-300ms, --contrast-ratio: 4.5:1+, --color-hierarchy: improved, --wcag-level: AA+
21 20 Bento Grids Design a Bento Grid layout. Use: modular grid system, rounded corners (16-24px), different card sizes (1x1, 2x1, 2x2), card-based hierarchy, soft backgrounds (#F5F5F7), subtle borders, content-first, Apple-style aesthetic. display: grid, grid-template-columns: repeat(auto-fit, minmax(...)), gap: 1rem, border-radius: 20px, background: #FFF, box-shadow: subtle ☐ Grid layout (CSS Grid), ☐ Rounded corners 16-24px, ☐ Varied card spans, ☐ Content fits card size, ☐ Responsive re-flow, ☐ Apple-like aesthetic --grid-gap: 20px, --card-radius: 24px, --card-bg: #FFFFFF, --page-bg: #F5F5F7, --shadow: soft
22 21 Neubrutalism Design a neubrutalist interface. Use: high contrast, hard black borders (3px+), bright pop colors, no blur, sharp or slightly rounded corners, bold typography, hard shadows (offset 4px 4px), raw aesthetic but functional. border: 3px solid black, box-shadow: 5px 5px 0px black, colors: #FFDB58 #FF6B6B #4ECDC4, font-weight: 700, no gradients ☐ Hard borders (2-4px), ☐ Hard offset shadows, ☐ High saturation colors, ☐ Bold typography, ☐ No blurs/gradients, ☐ Distinctive 'ugly-cute' look --border-width: 3px, --shadow-offset: 4px, --shadow-color: #000, --colors: high saturation, --font: bold sans
23 22 HUD / Sci-Fi FUI Design a futuristic HUD (Heads Up Display) or FUI. Use: thin lines (1px), neon cyan/blue on black, technical markers, decorative brackets, data visualization, monospaced tech fonts, glowing elements, transparency. border: 1px solid rgba(0,255,255,0.5), color: #00FFFF, background: transparent or rgba(0,0,0,0.8), font-family: monospace, text-shadow: 0 0 5px cyan ☐ Fine lines 1px, ☐ Neon glow text/borders, ☐ Monospaced font, ☐ Dark/Transparent BG, ☐ Decorative tech markers, ☐ Holographic feel --hud-color: #00FFFF, --bg-color: rgba(0,10,20,0.9), --line-width: 1px, --glow: 0 0 5px, --font: monospace

View File

@@ -1,53 +1,53 @@
No,Category,Guideline,Description,Do,Don't,Code Good,Code Bad,Severity,Docs URL No,Category,Guideline,Description,Do,Don't,Code Good,Code Bad,Severity,Docs URL
1,Widgets,Use StatelessWidget when possible,Immutable widgets are simpler,StatelessWidget for static UI,StatefulWidget for everything,class MyWidget extends StatelessWidget,class MyWidget extends StatefulWidget (static),Medium,https://api.flutter.dev/flutter/widgets/StatelessWidget-class.html 1,Widgets,Use StatelessWidget when possible,Immutable widgets are simpler,StatelessWidget for static UI,StatefulWidget for everything,class MyWidget extends StatelessWidget,class MyWidget extends StatefulWidget (static),Medium,https://api.flutter.dev/flutter/widgets/StatelessWidget-class.html
2,Widgets,Keep widgets small,Single responsibility principle,Extract widgets into smaller pieces,Large build methods,Column(children: [Header() Content()]),500+ line build method,Medium, 2,Widgets,Keep widgets small,Single responsibility principle,Extract widgets into smaller pieces,Large build methods,Column(children: [Header() Content()]),500+ line build method,Medium,
3,Widgets,Use const constructors,Compile-time constants for performance,const MyWidget() when possible,Non-const for static widgets,const Text('Hello'),Text('Hello') for literals,High,https://dart.dev/guides/language/language-tour#constant-constructors 3,Widgets,Use const constructors,Compile-time constants for performance,const MyWidget() when possible,Non-const for static widgets,const Text('Hello'),Text('Hello') for literals,High,https://dart.dev/guides/language/language-tour#constant-constructors
4,Widgets,Prefer composition over inheritance,Combine widgets using children,Compose widgets,Extend widget classes,Container(child: MyContent()),class MyContainer extends Container,Medium, 4,Widgets,Prefer composition over inheritance,Combine widgets using children,Compose widgets,Extend widget classes,Container(child: MyContent()),class MyContainer extends Container,Medium,
5,State,Use setState correctly,Minimal state in StatefulWidget,setState for UI state changes,setState for business logic,setState(() { _counter++; }),Complex logic in setState,Medium,https://api.flutter.dev/flutter/widgets/State/setState.html 5,State,Use setState correctly,Minimal state in StatefulWidget,setState for UI state changes,setState for business logic,setState(() { _counter++; }),Complex logic in setState,Medium,https://api.flutter.dev/flutter/widgets/State/setState.html
6,State,Avoid setState in build,Never call setState during build,setState in callbacks only,setState in build method,onPressed: () => setState(() {}),build() { setState(); },High, 6,State,Avoid setState in build,Never call setState during build,setState in callbacks only,setState in build method,onPressed: () => setState(() {}),build() { setState(); },High,
7,State,Use state management for complex apps,Provider Riverpod BLoC,State management for shared state,setState for global state,Provider.of<MyState>(context),Global setState calls,Medium, 7,State,Use state management for complex apps,Provider Riverpod BLoC,State management for shared state,setState for global state,Provider.of<MyState>(context),Global setState calls,Medium,
8,State,Prefer Riverpod or Provider,Recommended state solutions,Riverpod for new projects,InheritedWidget manually,ref.watch(myProvider),Custom InheritedWidget,Medium,https://riverpod.dev/ 8,State,Prefer Riverpod or Provider,Recommended state solutions,Riverpod for new projects,InheritedWidget manually,ref.watch(myProvider),Custom InheritedWidget,Medium,https://riverpod.dev/
9,State,Dispose resources,Clean up controllers and subscriptions,dispose() for cleanup,Memory leaks from subscriptions,@override void dispose() { controller.dispose(); },No dispose implementation,High, 9,State,Dispose resources,Clean up controllers and subscriptions,dispose() for cleanup,Memory leaks from subscriptions,@override void dispose() { controller.dispose(); },No dispose implementation,High,
10,Layout,Use Column and Row,Basic layout widgets,Column Row for linear layouts,Stack for simple layouts,"Column(children: [Text(), Button()])",Stack for vertical list,Medium,https://api.flutter.dev/flutter/widgets/Column-class.html 10,Layout,Use Column and Row,Basic layout widgets,Column Row for linear layouts,Stack for simple layouts,"Column(children: [Text(), Button()])",Stack for vertical list,Medium,https://api.flutter.dev/flutter/widgets/Column-class.html
11,Layout,Use Expanded and Flexible,Control flex behavior,Expanded to fill space,Fixed sizes in flex containers,Expanded(child: Container()),Container(width: 200) in Row,Medium, 11,Layout,Use Expanded and Flexible,Control flex behavior,Expanded to fill space,Fixed sizes in flex containers,Expanded(child: Container()),Container(width: 200) in Row,Medium,
12,Layout,Use SizedBox for spacing,Consistent spacing,SizedBox for gaps,Container for spacing only,SizedBox(height: 16),Container(height: 16),Low, 12,Layout,Use SizedBox for spacing,Consistent spacing,SizedBox for gaps,Container for spacing only,SizedBox(height: 16),Container(height: 16),Low,
13,Layout,Use LayoutBuilder for responsive,Respond to constraints,LayoutBuilder for adaptive layouts,Fixed sizes for responsive,LayoutBuilder(builder: (context constraints) {}),Container(width: 375),Medium,https://api.flutter.dev/flutter/widgets/LayoutBuilder-class.html 13,Layout,Use LayoutBuilder for responsive,Respond to constraints,LayoutBuilder for adaptive layouts,Fixed sizes for responsive,LayoutBuilder(builder: (context constraints) {}),Container(width: 375),Medium,https://api.flutter.dev/flutter/widgets/LayoutBuilder-class.html
14,Layout,Avoid deep nesting,Keep widget tree shallow,Extract deeply nested widgets,10+ levels of nesting,Extract widget to method or class,Column(Row(Column(Row(...)))),Medium, 14,Layout,Avoid deep nesting,Keep widget tree shallow,Extract deeply nested widgets,10+ levels of nesting,Extract widget to method or class,Column(Row(Column(Row(...)))),Medium,
15,Lists,Use ListView.builder,Lazy list building,ListView.builder for long lists,ListView with children for large lists,"ListView.builder(itemCount: 100, itemBuilder: ...)",ListView(children: items.map(...).toList()),High,https://api.flutter.dev/flutter/widgets/ListView-class.html 15,Lists,Use ListView.builder,Lazy list building,ListView.builder for long lists,ListView with children for large lists,"ListView.builder(itemCount: 100, itemBuilder: ...)",ListView(children: items.map(...).toList()),High,https://api.flutter.dev/flutter/widgets/ListView-class.html
16,Lists,Provide itemExtent when known,Skip measurement,itemExtent for fixed height items,No itemExtent for uniform lists,ListView.builder(itemExtent: 50),ListView.builder without itemExtent,Medium, 16,Lists,Provide itemExtent when known,Skip measurement,itemExtent for fixed height items,No itemExtent for uniform lists,ListView.builder(itemExtent: 50),ListView.builder without itemExtent,Medium,
17,Lists,Use keys for stateful items,Preserve widget state,Key for stateful list items,No key for dynamic lists,ListTile(key: ValueKey(item.id)),ListTile without key,High, 17,Lists,Use keys for stateful items,Preserve widget state,Key for stateful list items,No key for dynamic lists,ListTile(key: ValueKey(item.id)),ListTile without key,High,
18,Lists,Use SliverList for custom scroll,Custom scroll effects,CustomScrollView with Slivers,Nested ListViews,CustomScrollView(slivers: [SliverList()]),ListView inside ListView,Medium,https://api.flutter.dev/flutter/widgets/SliverList-class.html 18,Lists,Use SliverList for custom scroll,Custom scroll effects,CustomScrollView with Slivers,Nested ListViews,CustomScrollView(slivers: [SliverList()]),ListView inside ListView,Medium,https://api.flutter.dev/flutter/widgets/SliverList-class.html
19,Navigation,Use Navigator 2.0 or GoRouter,Declarative routing,go_router for navigation,Navigator.push for complex apps,GoRouter(routes: [...]),Navigator.push everywhere,Medium,https://pub.dev/packages/go_router 19,Navigation,Use Navigator 2.0 or GoRouter,Declarative routing,go_router for navigation,Navigator.push for complex apps,GoRouter(routes: [...]),Navigator.push everywhere,Medium,https://pub.dev/packages/go_router
20,Navigation,Use named routes,Organized navigation,Named routes for clarity,Anonymous routes,Navigator.pushNamed(context '/home'),Navigator.push(context MaterialPageRoute()),Low, 20,Navigation,Use named routes,Organized navigation,Named routes for clarity,Anonymous routes,Navigator.pushNamed(context '/home'),Navigator.push(context MaterialPageRoute()),Low,
21,Navigation,Handle back button (PopScope),Android back behavior and predictive back (Android 14+),Use PopScope widget (WillPopScope is deprecated),Use WillPopScope,"PopScope(canPop: false, onPopInvoked: (didPop) => ...)",WillPopScope(onWillPop: ...),High,https://api.flutter.dev/flutter/widgets/PopScope-class.html 21,Navigation,Handle back button (PopScope),Android back behavior and predictive back (Android 14+),Use PopScope widget (WillPopScope is deprecated),Use WillPopScope,"PopScope(canPop: false, onPopInvoked: (didPop) => ...)",WillPopScope(onWillPop: ...),High,https://api.flutter.dev/flutter/widgets/PopScope-class.html
22,Navigation,Pass typed arguments,Type-safe route arguments,Typed route arguments,Dynamic arguments,MyRoute(id: '123'),arguments: {'id': '123'},Medium, 22,Navigation,Pass typed arguments,Type-safe route arguments,Typed route arguments,Dynamic arguments,MyRoute(id: '123'),arguments: {'id': '123'},Medium,
23,Async,Use FutureBuilder,Async UI building,FutureBuilder for async data,setState for async,FutureBuilder(future: fetchData()),fetchData().then((d) => setState()),Medium,https://api.flutter.dev/flutter/widgets/FutureBuilder-class.html 23,Async,Use FutureBuilder,Async UI building,FutureBuilder for async data,setState for async,FutureBuilder(future: fetchData()),fetchData().then((d) => setState()),Medium,https://api.flutter.dev/flutter/widgets/FutureBuilder-class.html
24,Async,Use StreamBuilder,Stream UI building,StreamBuilder for streams,Manual stream subscription,StreamBuilder(stream: myStream),stream.listen in initState,Medium,https://api.flutter.dev/flutter/widgets/StreamBuilder-class.html 24,Async,Use StreamBuilder,Stream UI building,StreamBuilder for streams,Manual stream subscription,StreamBuilder(stream: myStream),stream.listen in initState,Medium,https://api.flutter.dev/flutter/widgets/StreamBuilder-class.html
25,Async,Handle loading and error states,Complete async UI states,ConnectionState checks,Only success state,if (snapshot.connectionState == ConnectionState.waiting),No loading indicator,High, 25,Async,Handle loading and error states,Complete async UI states,ConnectionState checks,Only success state,if (snapshot.connectionState == ConnectionState.waiting),No loading indicator,High,
26,Async,Cancel subscriptions,Clean up stream subscriptions,Cancel in dispose,Memory leaks,subscription.cancel() in dispose,No subscription cleanup,High, 26,Async,Cancel subscriptions,Clean up stream subscriptions,Cancel in dispose,Memory leaks,subscription.cancel() in dispose,No subscription cleanup,High,
27,Theming,Use ThemeData,Consistent theming,ThemeData for app theme,Hardcoded colors,Theme.of(context).primaryColor,Color(0xFF123456) everywhere,Medium,https://api.flutter.dev/flutter/material/ThemeData-class.html 27,Theming,Use ThemeData,Consistent theming,ThemeData for app theme,Hardcoded colors,Theme.of(context).primaryColor,Color(0xFF123456) everywhere,Medium,https://api.flutter.dev/flutter/material/ThemeData-class.html
28,Theming,Use ColorScheme,Material 3 color system,ColorScheme for colors,Individual color properties,colorScheme: ColorScheme.fromSeed(),primaryColor: Colors.blue,Medium, 28,Theming,Use ColorScheme,Material 3 color system,ColorScheme for colors,Individual color properties,colorScheme: ColorScheme.fromSeed(),primaryColor: Colors.blue,Medium,
29,Theming,Access theme via context,Dynamic theme access,Theme.of(context),Static theme reference,Theme.of(context).textTheme.bodyLarge,TextStyle(fontSize: 16),Medium, 29,Theming,Access theme via context,Dynamic theme access,Theme.of(context),Static theme reference,Theme.of(context).textTheme.bodyLarge,TextStyle(fontSize: 16),Medium,
30,Theming,Support dark mode,Respect system theme,darkTheme in MaterialApp,Light theme only,"MaterialApp(theme: light, darkTheme: dark)",MaterialApp(theme: light),Medium, 30,Theming,Support dark mode,Respect system theme,darkTheme in MaterialApp,Light theme only,"MaterialApp(theme: light, darkTheme: dark)",MaterialApp(theme: light),Medium,
31,Animation,Use implicit animations,Simple animations,AnimatedContainer AnimatedOpacity,Explicit for simple transitions,AnimatedContainer(duration: Duration()),AnimationController for fade,Low,https://api.flutter.dev/flutter/widgets/AnimatedContainer-class.html 31,Animation,Use implicit animations,Simple animations,AnimatedContainer AnimatedOpacity,Explicit for simple transitions,AnimatedContainer(duration: Duration()),AnimationController for fade,Low,https://api.flutter.dev/flutter/widgets/AnimatedContainer-class.html
32,Animation,Use AnimationController for complex,Fine-grained control,AnimationController with Ticker,Implicit for complex sequences,AnimationController(vsync: this),AnimatedContainer for staggered,Medium, 32,Animation,Use AnimationController for complex,Fine-grained control,AnimationController with Ticker,Implicit for complex sequences,AnimationController(vsync: this),AnimatedContainer for staggered,Medium,
33,Animation,Dispose AnimationControllers,Clean up animation resources,dispose() for controllers,Memory leaks,controller.dispose() in dispose,No controller disposal,High, 33,Animation,Dispose AnimationControllers,Clean up animation resources,dispose() for controllers,Memory leaks,controller.dispose() in dispose,No controller disposal,High,
34,Animation,Use Hero for transitions,Shared element transitions,Hero for navigation animations,Manual shared element,Hero(tag: 'image' child: Image()),Custom shared element animation,Low,https://api.flutter.dev/flutter/widgets/Hero-class.html 34,Animation,Use Hero for transitions,Shared element transitions,Hero for navigation animations,Manual shared element,Hero(tag: 'image' child: Image()),Custom shared element animation,Low,https://api.flutter.dev/flutter/widgets/Hero-class.html
35,Forms,Use Form widget,Form validation,Form with GlobalKey,Individual validation,Form(key: _formKey child: ...),TextField without Form,Medium,https://api.flutter.dev/flutter/widgets/Form-class.html 35,Forms,Use Form widget,Form validation,Form with GlobalKey,Individual validation,Form(key: _formKey child: ...),TextField without Form,Medium,https://api.flutter.dev/flutter/widgets/Form-class.html
36,Forms,Use TextEditingController,Control text input,Controller for text fields,onChanged for all text,final controller = TextEditingController(),onChanged: (v) => setState(),Medium, 36,Forms,Use TextEditingController,Control text input,Controller for text fields,onChanged for all text,final controller = TextEditingController(),onChanged: (v) => setState(),Medium,
37,Forms,Validate on submit,Form validation flow,_formKey.currentState!.validate(),Skip validation,if (_formKey.currentState!.validate()),Submit without validation,High, 37,Forms,Validate on submit,Form validation flow,_formKey.currentState!.validate(),Skip validation,if (_formKey.currentState!.validate()),Submit without validation,High,
38,Forms,Dispose controllers,Clean up text controllers,dispose() for controllers,Memory leaks,controller.dispose() in dispose,No controller disposal,High, 38,Forms,Dispose controllers,Clean up text controllers,dispose() for controllers,Memory leaks,controller.dispose() in dispose,No controller disposal,High,
39,Performance,Use const widgets,Reduce rebuilds,const for static widgets,No const for literals,const Icon(Icons.add),Icon(Icons.add),High, 39,Performance,Use const widgets,Reduce rebuilds,const for static widgets,No const for literals,const Icon(Icons.add),Icon(Icons.add),High,
40,Performance,Avoid rebuilding entire tree,Minimal rebuild scope,Isolate changing widgets,setState on parent,Consumer only around changing widget,setState on root widget,High, 40,Performance,Avoid rebuilding entire tree,Minimal rebuild scope,Isolate changing widgets,setState on parent,Consumer only around changing widget,setState on root widget,High,
41,Performance,Use RepaintBoundary,Isolate repaints,RepaintBoundary for animations,Full screen repaints,RepaintBoundary(child: AnimatedWidget()),Animation without boundary,Medium,https://api.flutter.dev/flutter/widgets/RepaintBoundary-class.html 41,Performance,Use RepaintBoundary,Isolate repaints,RepaintBoundary for animations,Full screen repaints,RepaintBoundary(child: AnimatedWidget()),Animation without boundary,Medium,https://api.flutter.dev/flutter/widgets/RepaintBoundary-class.html
42,Performance,Profile with DevTools,Measure before optimizing,Flutter DevTools profiling,Guess at performance,DevTools performance tab,Optimize without measuring,Medium,https://docs.flutter.dev/tools/devtools 42,Performance,Profile with DevTools,Measure before optimizing,Flutter DevTools profiling,Guess at performance,DevTools performance tab,Optimize without measuring,Medium,https://docs.flutter.dev/tools/devtools
43,Accessibility,Use Semantics widget,Screen reader support,Semantics for accessibility,Missing accessibility info,Semantics(label: 'Submit button'),GestureDetector without semantics,High,https://api.flutter.dev/flutter/widgets/Semantics-class.html 43,Accessibility,Use Semantics widget,Screen reader support,Semantics for accessibility,Missing accessibility info,Semantics(label: 'Submit button'),GestureDetector without semantics,High,https://api.flutter.dev/flutter/widgets/Semantics-class.html
44,Accessibility,Support large fonts,MediaQuery text scaling,MediaQuery.textScaleFactor,Fixed font sizes,style: Theme.of(context).textTheme,TextStyle(fontSize: 14),High, 44,Accessibility,Support large fonts,MediaQuery text scaling,MediaQuery.textScaleFactor,Fixed font sizes,style: Theme.of(context).textTheme,TextStyle(fontSize: 14),High,
45,Accessibility,Test with screen readers,TalkBack and VoiceOver,Test accessibility regularly,Skip accessibility testing,Regular TalkBack testing,No screen reader testing,High, 45,Accessibility,Test with screen readers,TalkBack and VoiceOver,Test accessibility regularly,Skip accessibility testing,Regular TalkBack testing,No screen reader testing,High,
46,Testing,Use widget tests,Test widget behavior,WidgetTester for UI tests,Unit tests only,testWidgets('...' (tester) async {}),Only test() for UI,Medium,https://docs.flutter.dev/testing 46,Testing,Use widget tests,Test widget behavior,WidgetTester for UI tests,Unit tests only,testWidgets('...' (tester) async {}),Only test() for UI,Medium,https://docs.flutter.dev/testing
47,Testing,Use integration tests,Full app testing,integration_test package,Manual testing only,IntegrationTestWidgetsFlutterBinding,Manual E2E testing,Medium, 47,Testing,Use integration tests,Full app testing,integration_test package,Manual testing only,IntegrationTestWidgetsFlutterBinding,Manual E2E testing,Medium,
48,Testing,Mock dependencies,Isolate tests,Mockito or mocktail,Real dependencies in tests,when(mock.method()).thenReturn(),Real API calls in tests,Medium, 48,Testing,Mock dependencies,Isolate tests,Mockito or mocktail,Real dependencies in tests,when(mock.method()).thenReturn(),Real API calls in tests,Medium,
49,Platform,Use Platform checks,Platform-specific code,Platform.isIOS Platform.isAndroid,Same code for all platforms,if (Platform.isIOS) {},Hardcoded iOS behavior,Medium, 49,Platform,Use Platform checks,Platform-specific code,Platform.isIOS Platform.isAndroid,Same code for all platforms,if (Platform.isIOS) {},Hardcoded iOS behavior,Medium,
50,Platform,Use kIsWeb for web,Web platform detection,kIsWeb for web checks,Platform for web,if (kIsWeb) {},Platform.isWeb (doesn't exist),Medium, 50,Platform,Use kIsWeb for web,Web platform detection,kIsWeb for web checks,Platform for web,if (kIsWeb) {},Platform.isWeb (doesn't exist),Medium,
51,Packages,Use pub.dev packages,Community packages,Popular maintained packages,Custom implementations,cached_network_image,Custom image cache,Medium,https://pub.dev/ 51,Packages,Use pub.dev packages,Community packages,Popular maintained packages,Custom implementations,cached_network_image,Custom image cache,Medium,https://pub.dev/
52,Packages,Check package quality,Quality before adding,Pub points and popularity,Any package without review,100+ pub points,Unmaintained packages,Medium, 52,Packages,Check package quality,Quality before adding,Pub points and popularity,Any package without review,100+ pub points,Unmaintained packages,Medium,
1 No Category Guideline Description Do Don't Code Good Code Bad Severity Docs URL
2 1 Widgets Use StatelessWidget when possible Immutable widgets are simpler StatelessWidget for static UI StatefulWidget for everything class MyWidget extends StatelessWidget class MyWidget extends StatefulWidget (static) Medium https://api.flutter.dev/flutter/widgets/StatelessWidget-class.html
3 2 Widgets Keep widgets small Single responsibility principle Extract widgets into smaller pieces Large build methods Column(children: [Header() Content()]) 500+ line build method Medium
4 3 Widgets Use const constructors Compile-time constants for performance const MyWidget() when possible Non-const for static widgets const Text('Hello') Text('Hello') for literals High https://dart.dev/guides/language/language-tour#constant-constructors
5 4 Widgets Prefer composition over inheritance Combine widgets using children Compose widgets Extend widget classes Container(child: MyContent()) class MyContainer extends Container Medium
6 5 State Use setState correctly Minimal state in StatefulWidget setState for UI state changes setState for business logic setState(() { _counter++; }) Complex logic in setState Medium https://api.flutter.dev/flutter/widgets/State/setState.html
7 6 State Avoid setState in build Never call setState during build setState in callbacks only setState in build method onPressed: () => setState(() {}) build() { setState(); } High
8 7 State Use state management for complex apps Provider Riverpod BLoC State management for shared state setState for global state Provider.of<MyState>(context) Global setState calls Medium
9 8 State Prefer Riverpod or Provider Recommended state solutions Riverpod for new projects InheritedWidget manually ref.watch(myProvider) Custom InheritedWidget Medium https://riverpod.dev/
10 9 State Dispose resources Clean up controllers and subscriptions dispose() for cleanup Memory leaks from subscriptions @override void dispose() { controller.dispose(); } No dispose implementation High
11 10 Layout Use Column and Row Basic layout widgets Column Row for linear layouts Stack for simple layouts Column(children: [Text(), Button()]) Stack for vertical list Medium https://api.flutter.dev/flutter/widgets/Column-class.html
12 11 Layout Use Expanded and Flexible Control flex behavior Expanded to fill space Fixed sizes in flex containers Expanded(child: Container()) Container(width: 200) in Row Medium
13 12 Layout Use SizedBox for spacing Consistent spacing SizedBox for gaps Container for spacing only SizedBox(height: 16) Container(height: 16) Low
14 13 Layout Use LayoutBuilder for responsive Respond to constraints LayoutBuilder for adaptive layouts Fixed sizes for responsive LayoutBuilder(builder: (context constraints) {}) Container(width: 375) Medium https://api.flutter.dev/flutter/widgets/LayoutBuilder-class.html
15 14 Layout Avoid deep nesting Keep widget tree shallow Extract deeply nested widgets 10+ levels of nesting Extract widget to method or class Column(Row(Column(Row(...)))) Medium
16 15 Lists Use ListView.builder Lazy list building ListView.builder for long lists ListView with children for large lists ListView.builder(itemCount: 100, itemBuilder: ...) ListView(children: items.map(...).toList()) High https://api.flutter.dev/flutter/widgets/ListView-class.html
17 16 Lists Provide itemExtent when known Skip measurement itemExtent for fixed height items No itemExtent for uniform lists ListView.builder(itemExtent: 50) ListView.builder without itemExtent Medium
18 17 Lists Use keys for stateful items Preserve widget state Key for stateful list items No key for dynamic lists ListTile(key: ValueKey(item.id)) ListTile without key High
19 18 Lists Use SliverList for custom scroll Custom scroll effects CustomScrollView with Slivers Nested ListViews CustomScrollView(slivers: [SliverList()]) ListView inside ListView Medium https://api.flutter.dev/flutter/widgets/SliverList-class.html
20 19 Navigation Use Navigator 2.0 or GoRouter Declarative routing go_router for navigation Navigator.push for complex apps GoRouter(routes: [...]) Navigator.push everywhere Medium https://pub.dev/packages/go_router
21 20 Navigation Use named routes Organized navigation Named routes for clarity Anonymous routes Navigator.pushNamed(context '/home') Navigator.push(context MaterialPageRoute()) Low
22 21 Navigation Handle back button (PopScope) Android back behavior and predictive back (Android 14+) Use PopScope widget (WillPopScope is deprecated) Use WillPopScope PopScope(canPop: false, onPopInvoked: (didPop) => ...) WillPopScope(onWillPop: ...) High https://api.flutter.dev/flutter/widgets/PopScope-class.html
23 22 Navigation Pass typed arguments Type-safe route arguments Typed route arguments Dynamic arguments MyRoute(id: '123') arguments: {'id': '123'} Medium
24 23 Async Use FutureBuilder Async UI building FutureBuilder for async data setState for async FutureBuilder(future: fetchData()) fetchData().then((d) => setState()) Medium https://api.flutter.dev/flutter/widgets/FutureBuilder-class.html
25 24 Async Use StreamBuilder Stream UI building StreamBuilder for streams Manual stream subscription StreamBuilder(stream: myStream) stream.listen in initState Medium https://api.flutter.dev/flutter/widgets/StreamBuilder-class.html
26 25 Async Handle loading and error states Complete async UI states ConnectionState checks Only success state if (snapshot.connectionState == ConnectionState.waiting) No loading indicator High
27 26 Async Cancel subscriptions Clean up stream subscriptions Cancel in dispose Memory leaks subscription.cancel() in dispose No subscription cleanup High
28 27 Theming Use ThemeData Consistent theming ThemeData for app theme Hardcoded colors Theme.of(context).primaryColor Color(0xFF123456) everywhere Medium https://api.flutter.dev/flutter/material/ThemeData-class.html
29 28 Theming Use ColorScheme Material 3 color system ColorScheme for colors Individual color properties colorScheme: ColorScheme.fromSeed() primaryColor: Colors.blue Medium
30 29 Theming Access theme via context Dynamic theme access Theme.of(context) Static theme reference Theme.of(context).textTheme.bodyLarge TextStyle(fontSize: 16) Medium
31 30 Theming Support dark mode Respect system theme darkTheme in MaterialApp Light theme only MaterialApp(theme: light, darkTheme: dark) MaterialApp(theme: light) Medium
32 31 Animation Use implicit animations Simple animations AnimatedContainer AnimatedOpacity Explicit for simple transitions AnimatedContainer(duration: Duration()) AnimationController for fade Low https://api.flutter.dev/flutter/widgets/AnimatedContainer-class.html
33 32 Animation Use AnimationController for complex Fine-grained control AnimationController with Ticker Implicit for complex sequences AnimationController(vsync: this) AnimatedContainer for staggered Medium
34 33 Animation Dispose AnimationControllers Clean up animation resources dispose() for controllers Memory leaks controller.dispose() in dispose No controller disposal High
35 34 Animation Use Hero for transitions Shared element transitions Hero for navigation animations Manual shared element Hero(tag: 'image' child: Image()) Custom shared element animation Low https://api.flutter.dev/flutter/widgets/Hero-class.html
36 35 Forms Use Form widget Form validation Form with GlobalKey Individual validation Form(key: _formKey child: ...) TextField without Form Medium https://api.flutter.dev/flutter/widgets/Form-class.html
37 36 Forms Use TextEditingController Control text input Controller for text fields onChanged for all text final controller = TextEditingController() onChanged: (v) => setState() Medium
38 37 Forms Validate on submit Form validation flow _formKey.currentState!.validate() Skip validation if (_formKey.currentState!.validate()) Submit without validation High
39 38 Forms Dispose controllers Clean up text controllers dispose() for controllers Memory leaks controller.dispose() in dispose No controller disposal High
40 39 Performance Use const widgets Reduce rebuilds const for static widgets No const for literals const Icon(Icons.add) Icon(Icons.add) High
41 40 Performance Avoid rebuilding entire tree Minimal rebuild scope Isolate changing widgets setState on parent Consumer only around changing widget setState on root widget High
42 41 Performance Use RepaintBoundary Isolate repaints RepaintBoundary for animations Full screen repaints RepaintBoundary(child: AnimatedWidget()) Animation without boundary Medium https://api.flutter.dev/flutter/widgets/RepaintBoundary-class.html
43 42 Performance Profile with DevTools Measure before optimizing Flutter DevTools profiling Guess at performance DevTools performance tab Optimize without measuring Medium https://docs.flutter.dev/tools/devtools
44 43 Accessibility Use Semantics widget Screen reader support Semantics for accessibility Missing accessibility info Semantics(label: 'Submit button') GestureDetector without semantics High https://api.flutter.dev/flutter/widgets/Semantics-class.html
45 44 Accessibility Support large fonts MediaQuery text scaling MediaQuery.textScaleFactor Fixed font sizes style: Theme.of(context).textTheme TextStyle(fontSize: 14) High
46 45 Accessibility Test with screen readers TalkBack and VoiceOver Test accessibility regularly Skip accessibility testing Regular TalkBack testing No screen reader testing High
47 46 Testing Use widget tests Test widget behavior WidgetTester for UI tests Unit tests only testWidgets('...' (tester) async {}) Only test() for UI Medium https://docs.flutter.dev/testing
48 47 Testing Use integration tests Full app testing integration_test package Manual testing only IntegrationTestWidgetsFlutterBinding Manual E2E testing Medium
49 48 Testing Mock dependencies Isolate tests Mockito or mocktail Real dependencies in tests when(mock.method()).thenReturn() Real API calls in tests Medium
50 49 Platform Use Platform checks Platform-specific code Platform.isIOS Platform.isAndroid Same code for all platforms if (Platform.isIOS) {} Hardcoded iOS behavior Medium
51 50 Platform Use kIsWeb for web Web platform detection kIsWeb for web checks Platform for web if (kIsWeb) {} Platform.isWeb (doesn't exist) Medium
52 51 Packages Use pub.dev packages Community packages Popular maintained packages Custom implementations cached_network_image Custom image cache Medium https://pub.dev/
53 52 Packages Check package quality Quality before adding Pub points and popularity Any package without review 100+ pub points Unmaintained packages Medium

View File

@@ -1,56 +1,56 @@
No,Category,Guideline,Description,Do,Don't,Code Good,Code Bad,Severity,Docs URL No,Category,Guideline,Description,Do,Don't,Code Good,Code Bad,Severity,Docs URL
1,Animation,Use Tailwind animate utilities,Built-in animations are optimized and respect reduced-motion,Use animate-pulse animate-spin animate-ping,Custom @keyframes for simple effects,animate-pulse,@keyframes pulse {...},Medium,https://tailwindcss.com/docs/animation 1,Animation,Use Tailwind animate utilities,Built-in animations are optimized and respect reduced-motion,Use animate-pulse animate-spin animate-ping,Custom @keyframes for simple effects,animate-pulse,@keyframes pulse {...},Medium,https://tailwindcss.com/docs/animation
2,Animation,Limit bounce animations,Continuous bounce is distracting and causes motion sickness,Use animate-bounce sparingly on CTAs only,Multiple bounce animations on page,Single CTA with animate-bounce,5+ elements with animate-bounce,High, 2,Animation,Limit bounce animations,Continuous bounce is distracting and causes motion sickness,Use animate-bounce sparingly on CTAs only,Multiple bounce animations on page,Single CTA with animate-bounce,5+ elements with animate-bounce,High,
3,Animation,Transition duration,Use appropriate transition speeds for UI feedback,duration-150 to duration-300 for UI,duration-1000 or longer for UI elements,transition-all duration-200,transition-all duration-1000,Medium,https://tailwindcss.com/docs/transition-duration 3,Animation,Transition duration,Use appropriate transition speeds for UI feedback,duration-150 to duration-300 for UI,duration-1000 or longer for UI elements,transition-all duration-200,transition-all duration-1000,Medium,https://tailwindcss.com/docs/transition-duration
4,Animation,Hover transitions,Add smooth transitions on hover state changes,Add transition class with hover states,Instant hover changes without transition,hover:bg-gray-100 transition-colors,hover:bg-gray-100 (no transition),Low, 4,Animation,Hover transitions,Add smooth transitions on hover state changes,Add transition class with hover states,Instant hover changes without transition,hover:bg-gray-100 transition-colors,hover:bg-gray-100 (no transition),Low,
5,Z-Index,Use Tailwind z-* scale,Consistent stacking context with predefined scale,z-0 z-10 z-20 z-30 z-40 z-50,Arbitrary z-index values,z-50 for modals,z-[9999],Medium,https://tailwindcss.com/docs/z-index 5,Z-Index,Use Tailwind z-* scale,Consistent stacking context with predefined scale,z-0 z-10 z-20 z-30 z-40 z-50,Arbitrary z-index values,z-50 for modals,z-[9999],Medium,https://tailwindcss.com/docs/z-index
6,Z-Index,Fixed elements z-index,Fixed navigation and modals need explicit z-index,z-50 for nav z-40 for dropdowns,Relying on DOM order for stacking,fixed top-0 z-50,fixed top-0 (no z-index),High, 6,Z-Index,Fixed elements z-index,Fixed navigation and modals need explicit z-index,z-50 for nav z-40 for dropdowns,Relying on DOM order for stacking,fixed top-0 z-50,fixed top-0 (no z-index),High,
7,Z-Index,Negative z-index for backgrounds,Use negative z-index for decorative backgrounds,z-[-1] for background elements,Positive z-index for backgrounds,-z-10 for decorative,z-10 for background,Low, 7,Z-Index,Negative z-index for backgrounds,Use negative z-index for decorative backgrounds,z-[-1] for background elements,Positive z-index for backgrounds,-z-10 for decorative,z-10 for background,Low,
8,Layout,Container max-width,Limit content width for readability,max-w-7xl mx-auto for main content,Full-width content on large screens,max-w-7xl mx-auto px-4,w-full (no max-width),Medium,https://tailwindcss.com/docs/container 8,Layout,Container max-width,Limit content width for readability,max-w-7xl mx-auto for main content,Full-width content on large screens,max-w-7xl mx-auto px-4,w-full (no max-width),Medium,https://tailwindcss.com/docs/container
9,Layout,Responsive padding,Adjust padding for different screen sizes,px-4 md:px-6 lg:px-8,Same padding all sizes,px-4 sm:px-6 lg:px-8,px-8 (same all sizes),Medium, 9,Layout,Responsive padding,Adjust padding for different screen sizes,px-4 md:px-6 lg:px-8,Same padding all sizes,px-4 sm:px-6 lg:px-8,px-8 (same all sizes),Medium,
10,Layout,Grid gaps,Use consistent gap utilities for spacing,gap-4 gap-6 gap-8,Margins on individual items,grid gap-6,grid with mb-4 on each item,Medium,https://tailwindcss.com/docs/gap 10,Layout,Grid gaps,Use consistent gap utilities for spacing,gap-4 gap-6 gap-8,Margins on individual items,grid gap-6,grid with mb-4 on each item,Medium,https://tailwindcss.com/docs/gap
11,Layout,Flexbox alignment,Use flex utilities for alignment,items-center justify-between,Multiple nested wrappers,flex items-center justify-between,Nested divs for alignment,Low, 11,Layout,Flexbox alignment,Use flex utilities for alignment,items-center justify-between,Multiple nested wrappers,flex items-center justify-between,Nested divs for alignment,Low,
12,Images,Aspect ratio,Maintain consistent image aspect ratios,aspect-video aspect-square,No aspect ratio on containers,aspect-video rounded-lg,No aspect control,Medium,https://tailwindcss.com/docs/aspect-ratio 12,Images,Aspect ratio,Maintain consistent image aspect ratios,aspect-video aspect-square,No aspect ratio on containers,aspect-video rounded-lg,No aspect control,Medium,https://tailwindcss.com/docs/aspect-ratio
13,Images,Object fit,Control image scaling within containers,object-cover object-contain,Stretched distorted images,object-cover w-full h-full,No object-fit,Medium,https://tailwindcss.com/docs/object-fit 13,Images,Object fit,Control image scaling within containers,object-cover object-contain,Stretched distorted images,object-cover w-full h-full,No object-fit,Medium,https://tailwindcss.com/docs/object-fit
14,Images,Lazy loading,Defer loading of off-screen images,loading='lazy' on images,All images eager load,<img loading='lazy'>,<img> without lazy,High, 14,Images,Lazy loading,Defer loading of off-screen images,loading='lazy' on images,All images eager load,<img loading='lazy'>,<img> without lazy,High,
15,Images,Responsive images,Serve appropriate image sizes,srcset and sizes attributes,Same large image all devices,srcset with multiple sizes,4000px image everywhere,High, 15,Images,Responsive images,Serve appropriate image sizes,srcset and sizes attributes,Same large image all devices,srcset with multiple sizes,4000px image everywhere,High,
16,Typography,Prose plugin,Use @tailwindcss/typography for rich text,prose prose-lg for article content,Custom styles for markdown,prose prose-lg max-w-none,Custom text styling,Medium,https://tailwindcss.com/docs/typography-plugin 16,Typography,Prose plugin,Use @tailwindcss/typography for rich text,prose prose-lg for article content,Custom styles for markdown,prose prose-lg max-w-none,Custom text styling,Medium,https://tailwindcss.com/docs/typography-plugin
17,Typography,Line height,Use appropriate line height for readability,leading-relaxed for body text,Default tight line height,leading-relaxed (1.625),leading-none or leading-tight,Medium,https://tailwindcss.com/docs/line-height 17,Typography,Line height,Use appropriate line height for readability,leading-relaxed for body text,Default tight line height,leading-relaxed (1.625),leading-none or leading-tight,Medium,https://tailwindcss.com/docs/line-height
18,Typography,Font size scale,Use consistent text size scale,text-sm text-base text-lg text-xl,Arbitrary font sizes,text-lg,text-[17px],Low,https://tailwindcss.com/docs/font-size 18,Typography,Font size scale,Use consistent text size scale,text-sm text-base text-lg text-xl,Arbitrary font sizes,text-lg,text-[17px],Low,https://tailwindcss.com/docs/font-size
19,Typography,Text truncation,Handle long text gracefully,truncate or line-clamp-*,Overflow breaking layout,line-clamp-2,No overflow handling,Medium,https://tailwindcss.com/docs/text-overflow 19,Typography,Text truncation,Handle long text gracefully,truncate or line-clamp-*,Overflow breaking layout,line-clamp-2,No overflow handling,Medium,https://tailwindcss.com/docs/text-overflow
20,Colors,Opacity utilities,Use color opacity utilities,bg-black/50 text-white/80,Separate opacity class,bg-black/50,bg-black opacity-50,Low,https://tailwindcss.com/docs/background-color 20,Colors,Opacity utilities,Use color opacity utilities,bg-black/50 text-white/80,Separate opacity class,bg-black/50,bg-black opacity-50,Low,https://tailwindcss.com/docs/background-color
21,Colors,Dark mode,Support dark mode with dark: prefix,dark:bg-gray-900 dark:text-white,No dark mode support,dark:bg-gray-900,Only light theme,Medium,https://tailwindcss.com/docs/dark-mode 21,Colors,Dark mode,Support dark mode with dark: prefix,dark:bg-gray-900 dark:text-white,No dark mode support,dark:bg-gray-900,Only light theme,Medium,https://tailwindcss.com/docs/dark-mode
22,Colors,Semantic colors,Use semantic color naming in config,primary secondary danger success,Generic color names in components,bg-primary,bg-blue-500 everywhere,Medium, 22,Colors,Semantic colors,Use semantic color naming in config,primary secondary danger success,Generic color names in components,bg-primary,bg-blue-500 everywhere,Medium,
23,Spacing,Consistent spacing scale,Use Tailwind spacing scale consistently,p-4 m-6 gap-8,Arbitrary pixel values,p-4 (1rem),p-[15px],Low,https://tailwindcss.com/docs/customizing-spacing 23,Spacing,Consistent spacing scale,Use Tailwind spacing scale consistently,p-4 m-6 gap-8,Arbitrary pixel values,p-4 (1rem),p-[15px],Low,https://tailwindcss.com/docs/customizing-spacing
24,Spacing,Negative margins,Use sparingly for overlapping effects,-mt-4 for overlapping elements,Negative margins for layout fixing,-mt-8 for card overlap,-m-2 to fix spacing issues,Medium, 24,Spacing,Negative margins,Use sparingly for overlapping effects,-mt-4 for overlapping elements,Negative margins for layout fixing,-mt-8 for card overlap,-m-2 to fix spacing issues,Medium,
25,Spacing,Space between,Use space-y-* for vertical lists,space-y-4 on flex/grid column,Margin on each child,space-y-4,Each child has mb-4,Low,https://tailwindcss.com/docs/space 25,Spacing,Space between,Use space-y-* for vertical lists,space-y-4 on flex/grid column,Margin on each child,space-y-4,Each child has mb-4,Low,https://tailwindcss.com/docs/space
26,Forms,Focus states,Always show focus indicators,focus:ring-2 focus:ring-blue-500,Remove focus outline,focus:ring-2 focus:ring-offset-2,focus:outline-none (no replacement),High, 26,Forms,Focus states,Always show focus indicators,focus:ring-2 focus:ring-blue-500,Remove focus outline,focus:ring-2 focus:ring-offset-2,focus:outline-none (no replacement),High,
27,Forms,Input sizing,Consistent input dimensions,h-10 px-3 for inputs,Inconsistent input heights,h-10 w-full px-3,Various heights per input,Medium, 27,Forms,Input sizing,Consistent input dimensions,h-10 px-3 for inputs,Inconsistent input heights,h-10 w-full px-3,Various heights per input,Medium,
28,Forms,Disabled states,Clear disabled styling,disabled:opacity-50 disabled:cursor-not-allowed,No disabled indication,disabled:opacity-50,Same style as enabled,Medium, 28,Forms,Disabled states,Clear disabled styling,disabled:opacity-50 disabled:cursor-not-allowed,No disabled indication,disabled:opacity-50,Same style as enabled,Medium,
29,Forms,Placeholder styling,Style placeholder text appropriately,placeholder:text-gray-400,Dark placeholder text,placeholder:text-gray-400,Default dark placeholder,Low, 29,Forms,Placeholder styling,Style placeholder text appropriately,placeholder:text-gray-400,Dark placeholder text,placeholder:text-gray-400,Default dark placeholder,Low,
30,Responsive,Mobile-first approach,Start with mobile styles and add breakpoints,Default mobile + md: lg: xl:,Desktop-first approach,text-sm md:text-base,text-base max-md:text-sm,Medium,https://tailwindcss.com/docs/responsive-design 30,Responsive,Mobile-first approach,Start with mobile styles and add breakpoints,Default mobile + md: lg: xl:,Desktop-first approach,text-sm md:text-base,text-base max-md:text-sm,Medium,https://tailwindcss.com/docs/responsive-design
31,Responsive,Breakpoint testing,Test at standard breakpoints,320 375 768 1024 1280 1536,Only test on development device,Test all breakpoints,Single device testing,High, 31,Responsive,Breakpoint testing,Test at standard breakpoints,320 375 768 1024 1280 1536,Only test on development device,Test all breakpoints,Single device testing,High,
32,Responsive,Hidden/shown utilities,Control visibility per breakpoint,hidden md:block,Different content per breakpoint,hidden md:flex,Separate mobile/desktop components,Low,https://tailwindcss.com/docs/display 32,Responsive,Hidden/shown utilities,Control visibility per breakpoint,hidden md:block,Different content per breakpoint,hidden md:flex,Separate mobile/desktop components,Low,https://tailwindcss.com/docs/display
33,Buttons,Button sizing,Consistent button dimensions,px-4 py-2 or px-6 py-3,Inconsistent button sizes,px-4 py-2 text-sm,Various padding per button,Medium, 33,Buttons,Button sizing,Consistent button dimensions,px-4 py-2 or px-6 py-3,Inconsistent button sizes,px-4 py-2 text-sm,Various padding per button,Medium,
34,Buttons,Touch targets,Minimum 44px touch target on mobile,min-h-[44px] on mobile,Small buttons on mobile,min-h-[44px] min-w-[44px],h-8 w-8 on mobile,High, 34,Buttons,Touch targets,Minimum 44px touch target on mobile,min-h-[44px] on mobile,Small buttons on mobile,min-h-[44px] min-w-[44px],h-8 w-8 on mobile,High,
35,Buttons,Loading states,Show loading feedback,disabled + spinner icon,Clickable during loading,<Button disabled><Spinner/></Button>,Button without loading state,High, 35,Buttons,Loading states,Show loading feedback,disabled + spinner icon,Clickable during loading,<Button disabled><Spinner/></Button>,Button without loading state,High,
36,Buttons,Icon buttons,Accessible icon-only buttons,aria-label on icon buttons,Icon button without label,<button aria-label='Close'><XIcon/></button>,<button><XIcon/></button>,High, 36,Buttons,Icon buttons,Accessible icon-only buttons,aria-label on icon buttons,Icon button without label,<button aria-label='Close'><XIcon/></button>,<button><XIcon/></button>,High,
37,Cards,Card structure,Consistent card styling,rounded-lg shadow-md p-6,Inconsistent card styles,rounded-2xl shadow-lg p-6,Mixed card styling,Low, 37,Cards,Card structure,Consistent card styling,rounded-lg shadow-md p-6,Inconsistent card styles,rounded-2xl shadow-lg p-6,Mixed card styling,Low,
38,Cards,Card hover states,Interactive cards should have hover feedback,hover:shadow-lg transition-shadow,No hover on clickable cards,hover:shadow-xl transition-shadow,Static cards that are clickable,Medium, 38,Cards,Card hover states,Interactive cards should have hover feedback,hover:shadow-lg transition-shadow,No hover on clickable cards,hover:shadow-xl transition-shadow,Static cards that are clickable,Medium,
39,Cards,Card spacing,Consistent internal card spacing,space-y-4 for card content,Inconsistent internal spacing,space-y-4 or p-6,Mixed mb-2 mb-4 mb-6,Low, 39,Cards,Card spacing,Consistent internal card spacing,space-y-4 for card content,Inconsistent internal spacing,space-y-4 or p-6,Mixed mb-2 mb-4 mb-6,Low,
40,Accessibility,Screen reader text,Provide context for screen readers,sr-only for hidden labels,Missing context for icons,<span class='sr-only'>Close menu</span>,No label for icon button,High,https://tailwindcss.com/docs/screen-readers 40,Accessibility,Screen reader text,Provide context for screen readers,sr-only for hidden labels,Missing context for icons,<span class='sr-only'>Close menu</span>,No label for icon button,High,https://tailwindcss.com/docs/screen-readers
41,Accessibility,Focus visible,Show focus only for keyboard users,focus-visible:ring-2,Focus on all interactions,focus-visible:ring-2,focus:ring-2 (shows on click too),Medium, 41,Accessibility,Focus visible,Show focus only for keyboard users,focus-visible:ring-2,Focus on all interactions,focus-visible:ring-2,focus:ring-2 (shows on click too),Medium,
42,Accessibility,Reduced motion,Respect user motion preferences,motion-reduce:animate-none,Ignore motion preferences,motion-reduce:transition-none,No reduced motion support,High,https://tailwindcss.com/docs/hover-focus-and-other-states#prefers-reduced-motion 42,Accessibility,Reduced motion,Respect user motion preferences,motion-reduce:animate-none,Ignore motion preferences,motion-reduce:transition-none,No reduced motion support,High,https://tailwindcss.com/docs/hover-focus-and-other-states#prefers-reduced-motion
43,Performance,Configure content paths,Tailwind needs to know where classes are used,Use 'content' array in config,Use deprecated 'purge' option (v2),"content: ['./src/**/*.{js,ts,jsx,tsx}']",purge: [...],High,https://tailwindcss.com/docs/content-configuration 43,Performance,Configure content paths,Tailwind needs to know where classes are used,Use 'content' array in config,Use deprecated 'purge' option (v2),"content: ['./src/**/*.{js,ts,jsx,tsx}']",purge: [...],High,https://tailwindcss.com/docs/content-configuration
44,Performance,JIT mode,Use JIT for faster builds and smaller bundles,JIT enabled (default in v3),Full CSS in development,Tailwind v3 defaults,Tailwind v2 without JIT,Medium, 44,Performance,JIT mode,Use JIT for faster builds and smaller bundles,JIT enabled (default in v3),Full CSS in development,Tailwind v3 defaults,Tailwind v2 without JIT,Medium,
45,Performance,Avoid @apply bloat,Use @apply sparingly,Direct utilities in HTML,Heavy @apply usage,class='px-4 py-2 rounded',@apply px-4 py-2 rounded;,Low,https://tailwindcss.com/docs/reusing-styles 45,Performance,Avoid @apply bloat,Use @apply sparingly,Direct utilities in HTML,Heavy @apply usage,class='px-4 py-2 rounded',@apply px-4 py-2 rounded;,Low,https://tailwindcss.com/docs/reusing-styles
46,Plugins,Official plugins,Use official Tailwind plugins,@tailwindcss/forms typography aspect-ratio,Custom implementations,@tailwindcss/forms,Custom form reset CSS,Medium,https://tailwindcss.com/docs/plugins 46,Plugins,Official plugins,Use official Tailwind plugins,@tailwindcss/forms typography aspect-ratio,Custom implementations,@tailwindcss/forms,Custom form reset CSS,Medium,https://tailwindcss.com/docs/plugins
47,Plugins,Custom utilities,Create utilities for repeated patterns,Custom utility in config,Repeated arbitrary values,Custom shadow utility,"shadow-[0_4px_20px_rgba(0,0,0,0.1)] everywhere",Medium, 47,Plugins,Custom utilities,Create utilities for repeated patterns,Custom utility in config,Repeated arbitrary values,Custom shadow utility,"shadow-[0_4px_20px_rgba(0,0,0,0.1)] everywhere",Medium,
48,Layout,Container Queries,Use @container for component-based responsiveness,Use @container and @lg: etc.,Media queries for component internals,@container @lg:grid-cols-2,@media (min-width: ...) inside component,Medium,https://github.com/tailwindlabs/tailwindcss-container-queries 48,Layout,Container Queries,Use @container for component-based responsiveness,Use @container and @lg: etc.,Media queries for component internals,@container @lg:grid-cols-2,@media (min-width: ...) inside component,Medium,https://github.com/tailwindlabs/tailwindcss-container-queries
49,Interactivity,Group and Peer,Style based on parent/sibling state,group-hover peer-checked,JS for simple state interactions,group-hover:text-blue-500,onMouseEnter={() => setHover(true)},Low,https://tailwindcss.com/docs/hover-focus-and-other-states#styling-based-on-parent-state 49,Interactivity,Group and Peer,Style based on parent/sibling state,group-hover peer-checked,JS for simple state interactions,group-hover:text-blue-500,onMouseEnter={() => setHover(true)},Low,https://tailwindcss.com/docs/hover-focus-and-other-states#styling-based-on-parent-state
50,Customization,Arbitrary Values,Use [] for one-off values,w-[350px] for specific needs,Creating config for single use,top-[117px] (if strictly needed),style={{ top: '117px' }},Low,https://tailwindcss.com/docs/adding-custom-styles#using-arbitrary-values 50,Customization,Arbitrary Values,Use [] for one-off values,w-[350px] for specific needs,Creating config for single use,top-[117px] (if strictly needed),style={{ top: '117px' }},Low,https://tailwindcss.com/docs/adding-custom-styles#using-arbitrary-values
51,Colors,Theme color variables,Define colors in Tailwind theme and use directly,bg-primary text-success border-cta,bg-[var(--color-primary)] text-[var(--color-success)],bg-primary,bg-[var(--color-primary)],Medium,https://tailwindcss.com/docs/customizing-colors 51,Colors,Theme color variables,Define colors in Tailwind theme and use directly,bg-primary text-success border-cta,bg-[var(--color-primary)] text-[var(--color-success)],bg-primary,bg-[var(--color-primary)],Medium,https://tailwindcss.com/docs/customizing-colors
52,Colors,Use bg-linear-to-* for gradients,Tailwind v4 uses bg-linear-to-* syntax for gradients,bg-linear-to-r bg-linear-to-b,bg-gradient-to-* (deprecated in v4),bg-linear-to-r from-blue-500 to-purple-500,bg-gradient-to-r from-blue-500 to-purple-500,Medium,https://tailwindcss.com/docs/background-image 52,Colors,Use bg-linear-to-* for gradients,Tailwind v4 uses bg-linear-to-* syntax for gradients,bg-linear-to-r bg-linear-to-b,bg-gradient-to-* (deprecated in v4),bg-linear-to-r from-blue-500 to-purple-500,bg-gradient-to-r from-blue-500 to-purple-500,Medium,https://tailwindcss.com/docs/background-image
53,Layout,Use shrink-0 shorthand,Shorter class name for flex-shrink-0,shrink-0 shrink,flex-shrink-0 flex-shrink,shrink-0,flex-shrink-0,Low,https://tailwindcss.com/docs/flex-shrink 53,Layout,Use shrink-0 shorthand,Shorter class name for flex-shrink-0,shrink-0 shrink,flex-shrink-0 flex-shrink,shrink-0,flex-shrink-0,Low,https://tailwindcss.com/docs/flex-shrink
54,Layout,Use size-* for square dimensions,Single utility for equal width and height,size-4 size-8 size-12,Separate h-* w-* for squares,size-6,h-6 w-6,Low,https://tailwindcss.com/docs/size 54,Layout,Use size-* for square dimensions,Single utility for equal width and height,size-4 size-8 size-12,Separate h-* w-* for squares,size-6,h-6 w-6,Low,https://tailwindcss.com/docs/size
55,Images,SVG explicit dimensions,Add width/height attributes to SVGs to prevent layout shift before CSS loads,<svg class='size-6' width='24' height='24'>,SVG without explicit dimensions,<svg class='size-6' width='24' height='24'>,<svg class='size-6'>,High, 55,Images,SVG explicit dimensions,Add width/height attributes to SVGs to prevent layout shift before CSS loads,<svg class='size-6' width='24' height='24'>,SVG without explicit dimensions,<svg class='size-6' width='24' height='24'>,<svg class='size-6'>,High,
1 No Category Guideline Description Do Don't Code Good Code Bad Severity Docs URL
2 1 Animation Use Tailwind animate utilities Built-in animations are optimized and respect reduced-motion Use animate-pulse animate-spin animate-ping Custom @keyframes for simple effects animate-pulse @keyframes pulse {...} Medium https://tailwindcss.com/docs/animation
3 2 Animation Limit bounce animations Continuous bounce is distracting and causes motion sickness Use animate-bounce sparingly on CTAs only Multiple bounce animations on page Single CTA with animate-bounce 5+ elements with animate-bounce High
4 3 Animation Transition duration Use appropriate transition speeds for UI feedback duration-150 to duration-300 for UI duration-1000 or longer for UI elements transition-all duration-200 transition-all duration-1000 Medium https://tailwindcss.com/docs/transition-duration
5 4 Animation Hover transitions Add smooth transitions on hover state changes Add transition class with hover states Instant hover changes without transition hover:bg-gray-100 transition-colors hover:bg-gray-100 (no transition) Low
6 5 Z-Index Use Tailwind z-* scale Consistent stacking context with predefined scale z-0 z-10 z-20 z-30 z-40 z-50 Arbitrary z-index values z-50 for modals z-[9999] Medium https://tailwindcss.com/docs/z-index
7 6 Z-Index Fixed elements z-index Fixed navigation and modals need explicit z-index z-50 for nav z-40 for dropdowns Relying on DOM order for stacking fixed top-0 z-50 fixed top-0 (no z-index) High
8 7 Z-Index Negative z-index for backgrounds Use negative z-index for decorative backgrounds z-[-1] for background elements Positive z-index for backgrounds -z-10 for decorative z-10 for background Low
9 8 Layout Container max-width Limit content width for readability max-w-7xl mx-auto for main content Full-width content on large screens max-w-7xl mx-auto px-4 w-full (no max-width) Medium https://tailwindcss.com/docs/container
10 9 Layout Responsive padding Adjust padding for different screen sizes px-4 md:px-6 lg:px-8 Same padding all sizes px-4 sm:px-6 lg:px-8 px-8 (same all sizes) Medium
11 10 Layout Grid gaps Use consistent gap utilities for spacing gap-4 gap-6 gap-8 Margins on individual items grid gap-6 grid with mb-4 on each item Medium https://tailwindcss.com/docs/gap
12 11 Layout Flexbox alignment Use flex utilities for alignment items-center justify-between Multiple nested wrappers flex items-center justify-between Nested divs for alignment Low
13 12 Images Aspect ratio Maintain consistent image aspect ratios aspect-video aspect-square No aspect ratio on containers aspect-video rounded-lg No aspect control Medium https://tailwindcss.com/docs/aspect-ratio
14 13 Images Object fit Control image scaling within containers object-cover object-contain Stretched distorted images object-cover w-full h-full No object-fit Medium https://tailwindcss.com/docs/object-fit
15 14 Images Lazy loading Defer loading of off-screen images loading='lazy' on images All images eager load <img loading='lazy'> <img> without lazy High
16 15 Images Responsive images Serve appropriate image sizes srcset and sizes attributes Same large image all devices srcset with multiple sizes 4000px image everywhere High
17 16 Typography Prose plugin Use @tailwindcss/typography for rich text prose prose-lg for article content Custom styles for markdown prose prose-lg max-w-none Custom text styling Medium https://tailwindcss.com/docs/typography-plugin
18 17 Typography Line height Use appropriate line height for readability leading-relaxed for body text Default tight line height leading-relaxed (1.625) leading-none or leading-tight Medium https://tailwindcss.com/docs/line-height
19 18 Typography Font size scale Use consistent text size scale text-sm text-base text-lg text-xl Arbitrary font sizes text-lg text-[17px] Low https://tailwindcss.com/docs/font-size
20 19 Typography Text truncation Handle long text gracefully truncate or line-clamp-* Overflow breaking layout line-clamp-2 No overflow handling Medium https://tailwindcss.com/docs/text-overflow
21 20 Colors Opacity utilities Use color opacity utilities bg-black/50 text-white/80 Separate opacity class bg-black/50 bg-black opacity-50 Low https://tailwindcss.com/docs/background-color
22 21 Colors Dark mode Support dark mode with dark: prefix dark:bg-gray-900 dark:text-white No dark mode support dark:bg-gray-900 Only light theme Medium https://tailwindcss.com/docs/dark-mode
23 22 Colors Semantic colors Use semantic color naming in config primary secondary danger success Generic color names in components bg-primary bg-blue-500 everywhere Medium
24 23 Spacing Consistent spacing scale Use Tailwind spacing scale consistently p-4 m-6 gap-8 Arbitrary pixel values p-4 (1rem) p-[15px] Low https://tailwindcss.com/docs/customizing-spacing
25 24 Spacing Negative margins Use sparingly for overlapping effects -mt-4 for overlapping elements Negative margins for layout fixing -mt-8 for card overlap -m-2 to fix spacing issues Medium
26 25 Spacing Space between Use space-y-* for vertical lists space-y-4 on flex/grid column Margin on each child space-y-4 Each child has mb-4 Low https://tailwindcss.com/docs/space
27 26 Forms Focus states Always show focus indicators focus:ring-2 focus:ring-blue-500 Remove focus outline focus:ring-2 focus:ring-offset-2 focus:outline-none (no replacement) High
28 27 Forms Input sizing Consistent input dimensions h-10 px-3 for inputs Inconsistent input heights h-10 w-full px-3 Various heights per input Medium
29 28 Forms Disabled states Clear disabled styling disabled:opacity-50 disabled:cursor-not-allowed No disabled indication disabled:opacity-50 Same style as enabled Medium
30 29 Forms Placeholder styling Style placeholder text appropriately placeholder:text-gray-400 Dark placeholder text placeholder:text-gray-400 Default dark placeholder Low
31 30 Responsive Mobile-first approach Start with mobile styles and add breakpoints Default mobile + md: lg: xl: Desktop-first approach text-sm md:text-base text-base max-md:text-sm Medium https://tailwindcss.com/docs/responsive-design
32 31 Responsive Breakpoint testing Test at standard breakpoints 320 375 768 1024 1280 1536 Only test on development device Test all breakpoints Single device testing High
33 32 Responsive Hidden/shown utilities Control visibility per breakpoint hidden md:block Different content per breakpoint hidden md:flex Separate mobile/desktop components Low https://tailwindcss.com/docs/display
34 33 Buttons Button sizing Consistent button dimensions px-4 py-2 or px-6 py-3 Inconsistent button sizes px-4 py-2 text-sm Various padding per button Medium
35 34 Buttons Touch targets Minimum 44px touch target on mobile min-h-[44px] on mobile Small buttons on mobile min-h-[44px] min-w-[44px] h-8 w-8 on mobile High
36 35 Buttons Loading states Show loading feedback disabled + spinner icon Clickable during loading <Button disabled><Spinner/></Button> Button without loading state High
37 36 Buttons Icon buttons Accessible icon-only buttons aria-label on icon buttons Icon button without label <button aria-label='Close'><XIcon/></button> <button><XIcon/></button> High
38 37 Cards Card structure Consistent card styling rounded-lg shadow-md p-6 Inconsistent card styles rounded-2xl shadow-lg p-6 Mixed card styling Low
39 38 Cards Card hover states Interactive cards should have hover feedback hover:shadow-lg transition-shadow No hover on clickable cards hover:shadow-xl transition-shadow Static cards that are clickable Medium
40 39 Cards Card spacing Consistent internal card spacing space-y-4 for card content Inconsistent internal spacing space-y-4 or p-6 Mixed mb-2 mb-4 mb-6 Low
41 40 Accessibility Screen reader text Provide context for screen readers sr-only for hidden labels Missing context for icons <span class='sr-only'>Close menu</span> No label for icon button High https://tailwindcss.com/docs/screen-readers
42 41 Accessibility Focus visible Show focus only for keyboard users focus-visible:ring-2 Focus on all interactions focus-visible:ring-2 focus:ring-2 (shows on click too) Medium
43 42 Accessibility Reduced motion Respect user motion preferences motion-reduce:animate-none Ignore motion preferences motion-reduce:transition-none No reduced motion support High https://tailwindcss.com/docs/hover-focus-and-other-states#prefers-reduced-motion
44 43 Performance Configure content paths Tailwind needs to know where classes are used Use 'content' array in config Use deprecated 'purge' option (v2) content: ['./src/**/*.{js,ts,jsx,tsx}'] purge: [...] High https://tailwindcss.com/docs/content-configuration
45 44 Performance JIT mode Use JIT for faster builds and smaller bundles JIT enabled (default in v3) Full CSS in development Tailwind v3 defaults Tailwind v2 without JIT Medium
46 45 Performance Avoid @apply bloat Use @apply sparingly Direct utilities in HTML Heavy @apply usage class='px-4 py-2 rounded' @apply px-4 py-2 rounded; Low https://tailwindcss.com/docs/reusing-styles
47 46 Plugins Official plugins Use official Tailwind plugins @tailwindcss/forms typography aspect-ratio Custom implementations @tailwindcss/forms Custom form reset CSS Medium https://tailwindcss.com/docs/plugins
48 47 Plugins Custom utilities Create utilities for repeated patterns Custom utility in config Repeated arbitrary values Custom shadow utility shadow-[0_4px_20px_rgba(0,0,0,0.1)] everywhere Medium
49 48 Layout Container Queries Use @container for component-based responsiveness Use @container and @lg: etc. Media queries for component internals @container @lg:grid-cols-2 @media (min-width: ...) inside component Medium https://github.com/tailwindlabs/tailwindcss-container-queries
50 49 Interactivity Group and Peer Style based on parent/sibling state group-hover peer-checked JS for simple state interactions group-hover:text-blue-500 onMouseEnter={() => setHover(true)} Low https://tailwindcss.com/docs/hover-focus-and-other-states#styling-based-on-parent-state
51 50 Customization Arbitrary Values Use [] for one-off values w-[350px] for specific needs Creating config for single use top-[117px] (if strictly needed) style={{ top: '117px' }} Low https://tailwindcss.com/docs/adding-custom-styles#using-arbitrary-values
52 51 Colors Theme color variables Define colors in Tailwind theme and use directly bg-primary text-success border-cta bg-[var(--color-primary)] text-[var(--color-success)] bg-primary bg-[var(--color-primary)] Medium https://tailwindcss.com/docs/customizing-colors
53 52 Colors Use bg-linear-to-* for gradients Tailwind v4 uses bg-linear-to-* syntax for gradients bg-linear-to-r bg-linear-to-b bg-gradient-to-* (deprecated in v4) bg-linear-to-r from-blue-500 to-purple-500 bg-gradient-to-r from-blue-500 to-purple-500 Medium https://tailwindcss.com/docs/background-image
54 53 Layout Use shrink-0 shorthand Shorter class name for flex-shrink-0 shrink-0 shrink flex-shrink-0 flex-shrink shrink-0 flex-shrink-0 Low https://tailwindcss.com/docs/flex-shrink
55 54 Layout Use size-* for square dimensions Single utility for equal width and height size-4 size-8 size-12 Separate h-* w-* for squares size-6 h-6 w-6 Low https://tailwindcss.com/docs/size
56 55 Images SVG explicit dimensions Add width/height attributes to SVGs to prevent layout shift before CSS loads <svg class='size-6' width='24' height='24'> SVG without explicit dimensions <svg class='size-6' width='24' height='24'> <svg class='size-6'> High

View File

@@ -1,53 +1,53 @@
No,Category,Guideline,Description,Do,Don't,Code Good,Code Bad,Severity,Docs URL No,Category,Guideline,Description,Do,Don't,Code Good,Code Bad,Severity,Docs URL
1,Routing,Use App Router for new projects,App Router is the recommended approach in Next.js 14+,app/ directory with page.tsx,pages/ for new projects,app/dashboard/page.tsx,pages/dashboard.tsx,Medium,https://nextjs.org/docs/app 1,Routing,Use App Router for new projects,App Router is the recommended approach in Next.js 14+,app/ directory with page.tsx,pages/ for new projects,app/dashboard/page.tsx,pages/dashboard.tsx,Medium,https://nextjs.org/docs/app
2,Routing,Use file-based routing,Create routes by adding files in app directory,page.tsx for routes layout.tsx for layouts,Manual route configuration,app/blog/[slug]/page.tsx,Custom router setup,Medium,https://nextjs.org/docs/app/building-your-application/routing 2,Routing,Use file-based routing,Create routes by adding files in app directory,page.tsx for routes layout.tsx for layouts,Manual route configuration,app/blog/[slug]/page.tsx,Custom router setup,Medium,https://nextjs.org/docs/app/building-your-application/routing
3,Routing,Colocate related files,Keep components styles tests with their routes,Component files alongside page.tsx,Separate components folder,app/dashboard/_components/,components/dashboard/,Low, 3,Routing,Colocate related files,Keep components styles tests with their routes,Component files alongside page.tsx,Separate components folder,app/dashboard/_components/,components/dashboard/,Low,
4,Routing,Use route groups for organization,Group routes without affecting URL,Parentheses for route groups,Nested folders affecting URL,(marketing)/about/page.tsx,marketing/about/page.tsx,Low,https://nextjs.org/docs/app/building-your-application/routing/route-groups 4,Routing,Use route groups for organization,Group routes without affecting URL,Parentheses for route groups,Nested folders affecting URL,(marketing)/about/page.tsx,marketing/about/page.tsx,Low,https://nextjs.org/docs/app/building-your-application/routing/route-groups
5,Routing,Handle loading states,Use loading.tsx for route loading UI,loading.tsx alongside page.tsx,Manual loading state management,app/dashboard/loading.tsx,useState for loading in page,Medium,https://nextjs.org/docs/app/building-your-application/routing/loading-ui-and-streaming 5,Routing,Handle loading states,Use loading.tsx for route loading UI,loading.tsx alongside page.tsx,Manual loading state management,app/dashboard/loading.tsx,useState for loading in page,Medium,https://nextjs.org/docs/app/building-your-application/routing/loading-ui-and-streaming
6,Routing,Handle errors with error.tsx,Catch errors at route level,error.tsx with reset function,try/catch in every component,app/dashboard/error.tsx,try/catch in page component,High,https://nextjs.org/docs/app/building-your-application/routing/error-handling 6,Routing,Handle errors with error.tsx,Catch errors at route level,error.tsx with reset function,try/catch in every component,app/dashboard/error.tsx,try/catch in page component,High,https://nextjs.org/docs/app/building-your-application/routing/error-handling
7,Rendering,Use Server Components by default,Server Components reduce client JS bundle,Keep components server by default,Add 'use client' unnecessarily,export default function Page(),('use client') for static content,High,https://nextjs.org/docs/app/building-your-application/rendering/server-components 7,Rendering,Use Server Components by default,Server Components reduce client JS bundle,Keep components server by default,Add 'use client' unnecessarily,export default function Page(),('use client') for static content,High,https://nextjs.org/docs/app/building-your-application/rendering/server-components
8,Rendering,Mark Client Components explicitly,'use client' for interactive components,Add 'use client' only when needed,Server Component with hooks/events,('use client') for onClick useState,No directive with useState,High,https://nextjs.org/docs/app/building-your-application/rendering/client-components 8,Rendering,Mark Client Components explicitly,'use client' for interactive components,Add 'use client' only when needed,Server Component with hooks/events,('use client') for onClick useState,No directive with useState,High,https://nextjs.org/docs/app/building-your-application/rendering/client-components
9,Rendering,Push Client Components down,Keep Client Components as leaf nodes,Client wrapper for interactive parts only,Mark page as Client Component,<InteractiveButton/> in Server Page,('use client') on page.tsx,High, 9,Rendering,Push Client Components down,Keep Client Components as leaf nodes,Client wrapper for interactive parts only,Mark page as Client Component,<InteractiveButton/> in Server Page,('use client') on page.tsx,High,
10,Rendering,Use streaming for better UX,Stream content with Suspense boundaries,Suspense for slow data fetches,Wait for all data before render,<Suspense><SlowComponent/></Suspense>,await allData then render,Medium,https://nextjs.org/docs/app/building-your-application/routing/loading-ui-and-streaming 10,Rendering,Use streaming for better UX,Stream content with Suspense boundaries,Suspense for slow data fetches,Wait for all data before render,<Suspense><SlowComponent/></Suspense>,await allData then render,Medium,https://nextjs.org/docs/app/building-your-application/routing/loading-ui-and-streaming
11,Rendering,Choose correct rendering strategy,SSG for static SSR for dynamic ISR for semi-static,generateStaticParams for known paths,SSR for static content,export const revalidate = 3600,fetch without cache config,Medium, 11,Rendering,Choose correct rendering strategy,SSG for static SSR for dynamic ISR for semi-static,generateStaticParams for known paths,SSR for static content,export const revalidate = 3600,fetch without cache config,Medium,
12,DataFetching,Fetch data in Server Components,Fetch directly in async Server Components,async function Page() { const data = await fetch() },useEffect for initial data,const data = await fetch(url),useEffect(() => fetch(url)),High,https://nextjs.org/docs/app/building-your-application/data-fetching 12,DataFetching,Fetch data in Server Components,Fetch directly in async Server Components,async function Page() { const data = await fetch() },useEffect for initial data,const data = await fetch(url),useEffect(() => fetch(url)),High,https://nextjs.org/docs/app/building-your-application/data-fetching
13,DataFetching,Configure caching explicitly (Next.js 15+),Next.js 15 changed defaults to uncached for fetch,Explicitly set cache: 'force-cache' for static data,Assume default is cached (it's not in Next.js 15),fetch(url { cache: 'force-cache' }),fetch(url) // Uncached in v15,High,https://nextjs.org/docs/app/building-your-application/upgrading/version-15 13,DataFetching,Configure caching explicitly (Next.js 15+),Next.js 15 changed defaults to uncached for fetch,Explicitly set cache: 'force-cache' for static data,Assume default is cached (it's not in Next.js 15),fetch(url { cache: 'force-cache' }),fetch(url) // Uncached in v15,High,https://nextjs.org/docs/app/building-your-application/upgrading/version-15
14,DataFetching,Deduplicate fetch requests,React and Next.js dedupe same requests,Same fetch call in multiple components,Manual request deduplication,Multiple components fetch same URL,Custom cache layer,Low, 14,DataFetching,Deduplicate fetch requests,React and Next.js dedupe same requests,Same fetch call in multiple components,Manual request deduplication,Multiple components fetch same URL,Custom cache layer,Low,
15,DataFetching,Use Server Actions for mutations,Server Actions for form submissions,action={serverAction} in forms,API route for every mutation,<form action={createPost}>,<form onSubmit={callApiRoute}>,Medium,https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations 15,DataFetching,Use Server Actions for mutations,Server Actions for form submissions,action={serverAction} in forms,API route for every mutation,<form action={createPost}>,<form onSubmit={callApiRoute}>,Medium,https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations
16,DataFetching,Revalidate data appropriately,Use revalidatePath/revalidateTag after mutations,Revalidate after Server Action,'use client' with manual refetch,revalidatePath('/posts'),router.refresh() everywhere,Medium,https://nextjs.org/docs/app/building-your-application/caching#revalidating 16,DataFetching,Revalidate data appropriately,Use revalidatePath/revalidateTag after mutations,Revalidate after Server Action,'use client' with manual refetch,revalidatePath('/posts'),router.refresh() everywhere,Medium,https://nextjs.org/docs/app/building-your-application/caching#revalidating
17,Images,Use next/image for optimization,Automatic image optimization and lazy loading,<Image> component for all images,<img> tags directly,<Image src={} alt={} width={} height={}>,<img src={}/>,High,https://nextjs.org/docs/app/building-your-application/optimizing/images 17,Images,Use next/image for optimization,Automatic image optimization and lazy loading,<Image> component for all images,<img> tags directly,<Image src={} alt={} width={} height={}>,<img src={}/>,High,https://nextjs.org/docs/app/building-your-application/optimizing/images
18,Images,Provide width and height,Prevent layout shift with dimensions,width and height props or fill,Missing dimensions,<Image width={400} height={300}/>,<Image src={url}/>,High, 18,Images,Provide width and height,Prevent layout shift with dimensions,width and height props or fill,Missing dimensions,<Image width={400} height={300}/>,<Image src={url}/>,High,
19,Images,Use fill for responsive images,Fill container with object-fit,fill prop with relative parent,Fixed dimensions for responsive,"<Image fill className=""object-cover""/>",<Image width={window.width}/>,Medium, 19,Images,Use fill for responsive images,Fill container with object-fit,fill prop with relative parent,Fixed dimensions for responsive,"<Image fill className=""object-cover""/>",<Image width={window.width}/>,Medium,
20,Images,Configure remote image domains,Whitelist external image sources,remotePatterns in next.config.js,Allow all domains,remotePatterns: [{ hostname: 'cdn.example.com' }],domains: ['*'],High,https://nextjs.org/docs/app/api-reference/components/image#remotepatterns 20,Images,Configure remote image domains,Whitelist external image sources,remotePatterns in next.config.js,Allow all domains,remotePatterns: [{ hostname: 'cdn.example.com' }],domains: ['*'],High,https://nextjs.org/docs/app/api-reference/components/image#remotepatterns
21,Images,Use priority for LCP images,Mark above-fold images as priority,priority prop on hero images,All images with priority,<Image priority src={hero}/>,<Image priority/> on every image,Medium, 21,Images,Use priority for LCP images,Mark above-fold images as priority,priority prop on hero images,All images with priority,<Image priority src={hero}/>,<Image priority/> on every image,Medium,
22,Fonts,Use next/font for fonts,Self-hosted fonts with zero layout shift,next/font/google or next/font/local,External font links,import { Inter } from 'next/font/google',"<link href=""fonts.googleapis.com""/>",Medium,https://nextjs.org/docs/app/building-your-application/optimizing/fonts 22,Fonts,Use next/font for fonts,Self-hosted fonts with zero layout shift,next/font/google or next/font/local,External font links,import { Inter } from 'next/font/google',"<link href=""fonts.googleapis.com""/>",Medium,https://nextjs.org/docs/app/building-your-application/optimizing/fonts
23,Fonts,Apply font to layout,Set font in root layout for consistency,className on body in layout.tsx,Font in individual pages,<body className={inter.className}>,Each page imports font,Low, 23,Fonts,Apply font to layout,Set font in root layout for consistency,className on body in layout.tsx,Font in individual pages,<body className={inter.className}>,Each page imports font,Low,
24,Fonts,Use variable fonts,Variable fonts reduce bundle size,Single variable font file,Multiple font weights as files,Inter({ subsets: ['latin'] }),Inter_400 Inter_500 Inter_700,Low, 24,Fonts,Use variable fonts,Variable fonts reduce bundle size,Single variable font file,Multiple font weights as files,Inter({ subsets: ['latin'] }),Inter_400 Inter_500 Inter_700,Low,
25,Metadata,Use generateMetadata for dynamic,Generate metadata based on params,export async function generateMetadata(),Hardcoded metadata everywhere,generateMetadata({ params }),export const metadata = {},Medium,https://nextjs.org/docs/app/building-your-application/optimizing/metadata 25,Metadata,Use generateMetadata for dynamic,Generate metadata based on params,export async function generateMetadata(),Hardcoded metadata everywhere,generateMetadata({ params }),export const metadata = {},Medium,https://nextjs.org/docs/app/building-your-application/optimizing/metadata
26,Metadata,Include OpenGraph images,Add OG images for social sharing,opengraph-image.tsx or og property,Missing social preview images,opengraph: { images: ['/og.png'] },No OG configuration,Medium, 26,Metadata,Include OpenGraph images,Add OG images for social sharing,opengraph-image.tsx or og property,Missing social preview images,opengraph: { images: ['/og.png'] },No OG configuration,Medium,
27,Metadata,Use metadata API,Export metadata object for static metadata,export const metadata = {},Manual head tags,export const metadata = { title: 'Page' },<head><title>Page</title></head>,Medium, 27,Metadata,Use metadata API,Export metadata object for static metadata,export const metadata = {},Manual head tags,export const metadata = { title: 'Page' },<head><title>Page</title></head>,Medium,
28,API,Use Route Handlers for APIs,app/api routes for API endpoints,app/api/users/route.ts,pages/api for new projects,export async function GET(request),export default function handler,Medium,https://nextjs.org/docs/app/building-your-application/routing/route-handlers 28,API,Use Route Handlers for APIs,app/api routes for API endpoints,app/api/users/route.ts,pages/api for new projects,export async function GET(request),export default function handler,Medium,https://nextjs.org/docs/app/building-your-application/routing/route-handlers
29,API,Return proper Response objects,Use NextResponse for API responses,NextResponse.json() for JSON,Plain objects or res.json(),return NextResponse.json({ data }),return { data },Medium, 29,API,Return proper Response objects,Use NextResponse for API responses,NextResponse.json() for JSON,Plain objects or res.json(),return NextResponse.json({ data }),return { data },Medium,
30,API,Handle HTTP methods explicitly,Export named functions for methods,Export GET POST PUT DELETE,Single handler for all methods,export async function POST(),switch(req.method),Low, 30,API,Handle HTTP methods explicitly,Export named functions for methods,Export GET POST PUT DELETE,Single handler for all methods,export async function POST(),switch(req.method),Low,
31,API,Validate request body,Validate input before processing,Zod or similar for validation,Trust client input,const body = schema.parse(await req.json()),const body = await req.json(),High, 31,API,Validate request body,Validate input before processing,Zod or similar for validation,Trust client input,const body = schema.parse(await req.json()),const body = await req.json(),High,
32,Middleware,Use middleware for auth,Protect routes with middleware.ts,middleware.ts at root,Auth check in every page,export function middleware(request),if (!session) redirect in page,Medium,https://nextjs.org/docs/app/building-your-application/routing/middleware 32,Middleware,Use middleware for auth,Protect routes with middleware.ts,middleware.ts at root,Auth check in every page,export function middleware(request),if (!session) redirect in page,Medium,https://nextjs.org/docs/app/building-your-application/routing/middleware
33,Middleware,Match specific paths,Configure middleware matcher,config.matcher for specific routes,Run middleware on all routes,matcher: ['/dashboard/:path*'],No matcher config,Medium, 33,Middleware,Match specific paths,Configure middleware matcher,config.matcher for specific routes,Run middleware on all routes,matcher: ['/dashboard/:path*'],No matcher config,Medium,
34,Middleware,Keep middleware edge-compatible,Middleware runs on Edge runtime,Edge-compatible code only,Node.js APIs in middleware,Edge-compatible auth check,fs.readFile in middleware,High, 34,Middleware,Keep middleware edge-compatible,Middleware runs on Edge runtime,Edge-compatible code only,Node.js APIs in middleware,Edge-compatible auth check,fs.readFile in middleware,High,
35,Environment,Use NEXT_PUBLIC prefix,Client-accessible env vars need prefix,NEXT_PUBLIC_ for client vars,Server vars exposed to client,NEXT_PUBLIC_API_URL,API_SECRET in client code,High,https://nextjs.org/docs/app/building-your-application/configuring/environment-variables 35,Environment,Use NEXT_PUBLIC prefix,Client-accessible env vars need prefix,NEXT_PUBLIC_ for client vars,Server vars exposed to client,NEXT_PUBLIC_API_URL,API_SECRET in client code,High,https://nextjs.org/docs/app/building-your-application/configuring/environment-variables
36,Environment,Validate env vars,Check required env vars exist,Validate on startup,Undefined env at runtime,if (!process.env.DATABASE_URL) throw,process.env.DATABASE_URL (might be undefined),High, 36,Environment,Validate env vars,Check required env vars exist,Validate on startup,Undefined env at runtime,if (!process.env.DATABASE_URL) throw,process.env.DATABASE_URL (might be undefined),High,
37,Environment,Use .env.local for secrets,Local env file for development secrets,.env.local gitignored,Secrets in .env committed,.env.local with secrets,.env with DATABASE_PASSWORD,High, 37,Environment,Use .env.local for secrets,Local env file for development secrets,.env.local gitignored,Secrets in .env committed,.env.local with secrets,.env with DATABASE_PASSWORD,High,
38,Performance,Analyze bundle size,Use @next/bundle-analyzer,Bundle analyzer in dev,Ship large bundles blindly,ANALYZE=true npm run build,No bundle analysis,Medium,https://nextjs.org/docs/app/building-your-application/optimizing/bundle-analyzer 38,Performance,Analyze bundle size,Use @next/bundle-analyzer,Bundle analyzer in dev,Ship large bundles blindly,ANALYZE=true npm run build,No bundle analysis,Medium,https://nextjs.org/docs/app/building-your-application/optimizing/bundle-analyzer
39,Performance,Use dynamic imports,Code split with next/dynamic,dynamic() for heavy components,Import everything statically,const Chart = dynamic(() => import('./Chart')),import Chart from './Chart',Medium,https://nextjs.org/docs/app/building-your-application/optimizing/lazy-loading 39,Performance,Use dynamic imports,Code split with next/dynamic,dynamic() for heavy components,Import everything statically,const Chart = dynamic(() => import('./Chart')),import Chart from './Chart',Medium,https://nextjs.org/docs/app/building-your-application/optimizing/lazy-loading
40,Performance,Avoid layout shifts,Reserve space for dynamic content,Skeleton loaders aspect ratios,Content popping in,"<Skeleton className=""h-48""/>",No placeholder for async content,High, 40,Performance,Avoid layout shifts,Reserve space for dynamic content,Skeleton loaders aspect ratios,Content popping in,"<Skeleton className=""h-48""/>",No placeholder for async content,High,
41,Performance,Use Partial Prerendering,Combine static and dynamic in one route,Static shell with Suspense holes,Full dynamic or static pages,Static header + dynamic content,Entire page SSR,Low,https://nextjs.org/docs/app/building-your-application/rendering/partial-prerendering 41,Performance,Use Partial Prerendering,Combine static and dynamic in one route,Static shell with Suspense holes,Full dynamic or static pages,Static header + dynamic content,Entire page SSR,Low,https://nextjs.org/docs/app/building-your-application/rendering/partial-prerendering
42,Link,Use next/link for navigation,Client-side navigation with prefetching,"<Link href=""""> for internal links",<a> for internal navigation,"<Link href=""/about"">About</Link>","<a href=""/about"">About</a>",High,https://nextjs.org/docs/app/api-reference/components/link 42,Link,Use next/link for navigation,Client-side navigation with prefetching,"<Link href=""""> for internal links",<a> for internal navigation,"<Link href=""/about"">About</Link>","<a href=""/about"">About</a>",High,https://nextjs.org/docs/app/api-reference/components/link
43,Link,Prefetch strategically,Control prefetching behavior,prefetch={false} for low-priority,Prefetch all links,<Link prefetch={false}>,Default prefetch on every link,Low, 43,Link,Prefetch strategically,Control prefetching behavior,prefetch={false} for low-priority,Prefetch all links,<Link prefetch={false}>,Default prefetch on every link,Low,
44,Link,Use scroll option appropriately,Control scroll behavior on navigation,scroll={false} for tabs pagination,Always scroll to top,<Link scroll={false}>,Manual scroll management,Low, 44,Link,Use scroll option appropriately,Control scroll behavior on navigation,scroll={false} for tabs pagination,Always scroll to top,<Link scroll={false}>,Manual scroll management,Low,
45,Config,Use next.config.js correctly,Configure Next.js behavior,Proper config options,Deprecated or wrong options,images: { remotePatterns: [] },images: { domains: [] },Medium,https://nextjs.org/docs/app/api-reference/next-config-js 45,Config,Use next.config.js correctly,Configure Next.js behavior,Proper config options,Deprecated or wrong options,images: { remotePatterns: [] },images: { domains: [] },Medium,https://nextjs.org/docs/app/api-reference/next-config-js
46,Config,Enable strict mode,Catch potential issues early,reactStrictMode: true,Strict mode disabled,reactStrictMode: true,reactStrictMode: false,Medium, 46,Config,Enable strict mode,Catch potential issues early,reactStrictMode: true,Strict mode disabled,reactStrictMode: true,reactStrictMode: false,Medium,
47,Config,Configure redirects and rewrites,Use config for URL management,redirects() rewrites() in config,Manual redirect handling,redirects: async () => [...],res.redirect in pages,Medium,https://nextjs.org/docs/app/api-reference/next-config-js/redirects 47,Config,Configure redirects and rewrites,Use config for URL management,redirects() rewrites() in config,Manual redirect handling,redirects: async () => [...],res.redirect in pages,Medium,https://nextjs.org/docs/app/api-reference/next-config-js/redirects
48,Deployment,Use Vercel for easiest deploy,Vercel optimized for Next.js,Deploy to Vercel,Self-host without knowledge,vercel deploy,Complex Docker setup for simple app,Low,https://nextjs.org/docs/app/building-your-application/deploying 48,Deployment,Use Vercel for easiest deploy,Vercel optimized for Next.js,Deploy to Vercel,Self-host without knowledge,vercel deploy,Complex Docker setup for simple app,Low,https://nextjs.org/docs/app/building-your-application/deploying
49,Deployment,Configure output for self-hosting,Set output option for deployment target,output: 'standalone' for Docker,Default output for containers,output: 'standalone',No output config for Docker,Medium,https://nextjs.org/docs/app/building-your-application/deploying#self-hosting 49,Deployment,Configure output for self-hosting,Set output option for deployment target,output: 'standalone' for Docker,Default output for containers,output: 'standalone',No output config for Docker,Medium,https://nextjs.org/docs/app/building-your-application/deploying#self-hosting
50,Security,Sanitize user input,Never trust user input,Escape sanitize validate all input,Direct interpolation of user data,DOMPurify.sanitize(userInput),dangerouslySetInnerHTML={{ __html: userInput }},High, 50,Security,Sanitize user input,Never trust user input,Escape sanitize validate all input,Direct interpolation of user data,DOMPurify.sanitize(userInput),dangerouslySetInnerHTML={{ __html: userInput }},High,
51,Security,Use CSP headers,Content Security Policy for XSS protection,Configure CSP in next.config.js,No security headers,headers() with CSP,No CSP configuration,High,https://nextjs.org/docs/app/building-your-application/configuring/content-security-policy 51,Security,Use CSP headers,Content Security Policy for XSS protection,Configure CSP in next.config.js,No security headers,headers() with CSP,No CSP configuration,High,https://nextjs.org/docs/app/building-your-application/configuring/content-security-policy
52,Security,Validate Server Action input,Server Actions are public endpoints,Validate and authorize in Server Action,Trust Server Action input,Auth check + validation in action,Direct database call without check,High, 52,Security,Validate Server Action input,Server Actions are public endpoints,Validate and authorize in Server Action,Trust Server Action input,Auth check + validation in action,Direct database call without check,High,
1 No Category Guideline Description Do Don't Code Good Code Bad Severity Docs URL
2 1 Routing Use App Router for new projects App Router is the recommended approach in Next.js 14+ app/ directory with page.tsx pages/ for new projects app/dashboard/page.tsx pages/dashboard.tsx Medium https://nextjs.org/docs/app
3 2 Routing Use file-based routing Create routes by adding files in app directory page.tsx for routes layout.tsx for layouts Manual route configuration app/blog/[slug]/page.tsx Custom router setup Medium https://nextjs.org/docs/app/building-your-application/routing
4 3 Routing Colocate related files Keep components styles tests with their routes Component files alongside page.tsx Separate components folder app/dashboard/_components/ components/dashboard/ Low
5 4 Routing Use route groups for organization Group routes without affecting URL Parentheses for route groups Nested folders affecting URL (marketing)/about/page.tsx marketing/about/page.tsx Low https://nextjs.org/docs/app/building-your-application/routing/route-groups
6 5 Routing Handle loading states Use loading.tsx for route loading UI loading.tsx alongside page.tsx Manual loading state management app/dashboard/loading.tsx useState for loading in page Medium https://nextjs.org/docs/app/building-your-application/routing/loading-ui-and-streaming
7 6 Routing Handle errors with error.tsx Catch errors at route level error.tsx with reset function try/catch in every component app/dashboard/error.tsx try/catch in page component High https://nextjs.org/docs/app/building-your-application/routing/error-handling
8 7 Rendering Use Server Components by default Server Components reduce client JS bundle Keep components server by default Add 'use client' unnecessarily export default function Page() ('use client') for static content High https://nextjs.org/docs/app/building-your-application/rendering/server-components
9 8 Rendering Mark Client Components explicitly 'use client' for interactive components Add 'use client' only when needed Server Component with hooks/events ('use client') for onClick useState No directive with useState High https://nextjs.org/docs/app/building-your-application/rendering/client-components
10 9 Rendering Push Client Components down Keep Client Components as leaf nodes Client wrapper for interactive parts only Mark page as Client Component <InteractiveButton/> in Server Page ('use client') on page.tsx High
11 10 Rendering Use streaming for better UX Stream content with Suspense boundaries Suspense for slow data fetches Wait for all data before render <Suspense><SlowComponent/></Suspense> await allData then render Medium https://nextjs.org/docs/app/building-your-application/routing/loading-ui-and-streaming
12 11 Rendering Choose correct rendering strategy SSG for static SSR for dynamic ISR for semi-static generateStaticParams for known paths SSR for static content export const revalidate = 3600 fetch without cache config Medium
13 12 DataFetching Fetch data in Server Components Fetch directly in async Server Components async function Page() { const data = await fetch() } useEffect for initial data const data = await fetch(url) useEffect(() => fetch(url)) High https://nextjs.org/docs/app/building-your-application/data-fetching
14 13 DataFetching Configure caching explicitly (Next.js 15+) Next.js 15 changed defaults to uncached for fetch Explicitly set cache: 'force-cache' for static data Assume default is cached (it's not in Next.js 15) fetch(url { cache: 'force-cache' }) fetch(url) // Uncached in v15 High https://nextjs.org/docs/app/building-your-application/upgrading/version-15
15 14 DataFetching Deduplicate fetch requests React and Next.js dedupe same requests Same fetch call in multiple components Manual request deduplication Multiple components fetch same URL Custom cache layer Low
16 15 DataFetching Use Server Actions for mutations Server Actions for form submissions action={serverAction} in forms API route for every mutation <form action={createPost}> <form onSubmit={callApiRoute}> Medium https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations
17 16 DataFetching Revalidate data appropriately Use revalidatePath/revalidateTag after mutations Revalidate after Server Action 'use client' with manual refetch revalidatePath('/posts') router.refresh() everywhere Medium https://nextjs.org/docs/app/building-your-application/caching#revalidating
18 17 Images Use next/image for optimization Automatic image optimization and lazy loading <Image> component for all images <img> tags directly <Image src={} alt={} width={} height={}> <img src={}/> High https://nextjs.org/docs/app/building-your-application/optimizing/images
19 18 Images Provide width and height Prevent layout shift with dimensions width and height props or fill Missing dimensions <Image width={400} height={300}/> <Image src={url}/> High
20 19 Images Use fill for responsive images Fill container with object-fit fill prop with relative parent Fixed dimensions for responsive <Image fill className="object-cover"/> <Image width={window.width}/> Medium
21 20 Images Configure remote image domains Whitelist external image sources remotePatterns in next.config.js Allow all domains remotePatterns: [{ hostname: 'cdn.example.com' }] domains: ['*'] High https://nextjs.org/docs/app/api-reference/components/image#remotepatterns
22 21 Images Use priority for LCP images Mark above-fold images as priority priority prop on hero images All images with priority <Image priority src={hero}/> <Image priority/> on every image Medium
23 22 Fonts Use next/font for fonts Self-hosted fonts with zero layout shift next/font/google or next/font/local External font links import { Inter } from 'next/font/google' <link href="fonts.googleapis.com"/> Medium https://nextjs.org/docs/app/building-your-application/optimizing/fonts
24 23 Fonts Apply font to layout Set font in root layout for consistency className on body in layout.tsx Font in individual pages <body className={inter.className}> Each page imports font Low
25 24 Fonts Use variable fonts Variable fonts reduce bundle size Single variable font file Multiple font weights as files Inter({ subsets: ['latin'] }) Inter_400 Inter_500 Inter_700 Low
26 25 Metadata Use generateMetadata for dynamic Generate metadata based on params export async function generateMetadata() Hardcoded metadata everywhere generateMetadata({ params }) export const metadata = {} Medium https://nextjs.org/docs/app/building-your-application/optimizing/metadata
27 26 Metadata Include OpenGraph images Add OG images for social sharing opengraph-image.tsx or og property Missing social preview images opengraph: { images: ['/og.png'] } No OG configuration Medium
28 27 Metadata Use metadata API Export metadata object for static metadata export const metadata = {} Manual head tags export const metadata = { title: 'Page' } <head><title>Page</title></head> Medium
29 28 API Use Route Handlers for APIs app/api routes for API endpoints app/api/users/route.ts pages/api for new projects export async function GET(request) export default function handler Medium https://nextjs.org/docs/app/building-your-application/routing/route-handlers
30 29 API Return proper Response objects Use NextResponse for API responses NextResponse.json() for JSON Plain objects or res.json() return NextResponse.json({ data }) return { data } Medium
31 30 API Handle HTTP methods explicitly Export named functions for methods Export GET POST PUT DELETE Single handler for all methods export async function POST() switch(req.method) Low
32 31 API Validate request body Validate input before processing Zod or similar for validation Trust client input const body = schema.parse(await req.json()) const body = await req.json() High
33 32 Middleware Use middleware for auth Protect routes with middleware.ts middleware.ts at root Auth check in every page export function middleware(request) if (!session) redirect in page Medium https://nextjs.org/docs/app/building-your-application/routing/middleware
34 33 Middleware Match specific paths Configure middleware matcher config.matcher for specific routes Run middleware on all routes matcher: ['/dashboard/:path*'] No matcher config Medium
35 34 Middleware Keep middleware edge-compatible Middleware runs on Edge runtime Edge-compatible code only Node.js APIs in middleware Edge-compatible auth check fs.readFile in middleware High
36 35 Environment Use NEXT_PUBLIC prefix Client-accessible env vars need prefix NEXT_PUBLIC_ for client vars Server vars exposed to client NEXT_PUBLIC_API_URL API_SECRET in client code High https://nextjs.org/docs/app/building-your-application/configuring/environment-variables
37 36 Environment Validate env vars Check required env vars exist Validate on startup Undefined env at runtime if (!process.env.DATABASE_URL) throw process.env.DATABASE_URL (might be undefined) High
38 37 Environment Use .env.local for secrets Local env file for development secrets .env.local gitignored Secrets in .env committed .env.local with secrets .env with DATABASE_PASSWORD High
39 38 Performance Analyze bundle size Use @next/bundle-analyzer Bundle analyzer in dev Ship large bundles blindly ANALYZE=true npm run build No bundle analysis Medium https://nextjs.org/docs/app/building-your-application/optimizing/bundle-analyzer
40 39 Performance Use dynamic imports Code split with next/dynamic dynamic() for heavy components Import everything statically const Chart = dynamic(() => import('./Chart')) import Chart from './Chart' Medium https://nextjs.org/docs/app/building-your-application/optimizing/lazy-loading
41 40 Performance Avoid layout shifts Reserve space for dynamic content Skeleton loaders aspect ratios Content popping in <Skeleton className="h-48"/> No placeholder for async content High
42 41 Performance Use Partial Prerendering Combine static and dynamic in one route Static shell with Suspense holes Full dynamic or static pages Static header + dynamic content Entire page SSR Low https://nextjs.org/docs/app/building-your-application/rendering/partial-prerendering
43 42 Link Use next/link for navigation Client-side navigation with prefetching <Link href=""> for internal links <a> for internal navigation <Link href="/about">About</Link> <a href="/about">About</a> High https://nextjs.org/docs/app/api-reference/components/link
44 43 Link Prefetch strategically Control prefetching behavior prefetch={false} for low-priority Prefetch all links <Link prefetch={false}> Default prefetch on every link Low
45 44 Link Use scroll option appropriately Control scroll behavior on navigation scroll={false} for tabs pagination Always scroll to top <Link scroll={false}> Manual scroll management Low
46 45 Config Use next.config.js correctly Configure Next.js behavior Proper config options Deprecated or wrong options images: { remotePatterns: [] } images: { domains: [] } Medium https://nextjs.org/docs/app/api-reference/next-config-js
47 46 Config Enable strict mode Catch potential issues early reactStrictMode: true Strict mode disabled reactStrictMode: true reactStrictMode: false Medium
48 47 Config Configure redirects and rewrites Use config for URL management redirects() rewrites() in config Manual redirect handling redirects: async () => [...] res.redirect in pages Medium https://nextjs.org/docs/app/api-reference/next-config-js/redirects
49 48 Deployment Use Vercel for easiest deploy Vercel optimized for Next.js Deploy to Vercel Self-host without knowledge vercel deploy Complex Docker setup for simple app Low https://nextjs.org/docs/app/building-your-application/deploying
50 49 Deployment Configure output for self-hosting Set output option for deployment target output: 'standalone' for Docker Default output for containers output: 'standalone' No output config for Docker Medium https://nextjs.org/docs/app/building-your-application/deploying#self-hosting
51 50 Security Sanitize user input Never trust user input Escape sanitize validate all input Direct interpolation of user data DOMPurify.sanitize(userInput) dangerouslySetInnerHTML={{ __html: userInput }} High
52 51 Security Use CSP headers Content Security Policy for XSS protection Configure CSP in next.config.js No security headers headers() with CSP No CSP configuration High https://nextjs.org/docs/app/building-your-application/configuring/content-security-policy
53 52 Security Validate Server Action input Server Actions are public endpoints Validate and authorize in Server Action Trust Server Action input Auth check + validation in action Direct database call without check High

View File

@@ -1,52 +1,52 @@
No,Category,Guideline,Description,Do,Don't,Code Good,Code Bad,Severity,Docs URL No,Category,Guideline,Description,Do,Don't,Code Good,Code Bad,Severity,Docs URL
1,Components,Use functional components,Hooks-based components are standard,Functional components with hooks,Class components,const App = () => { },class App extends Component,Medium,https://reactnative.dev/docs/intro-react 1,Components,Use functional components,Hooks-based components are standard,Functional components with hooks,Class components,const App = () => { },class App extends Component,Medium,https://reactnative.dev/docs/intro-react
2,Components,Keep components small,Single responsibility principle,Split into smaller components,Large monolithic components,<Header /><Content /><Footer />,500+ line component,Medium, 2,Components,Keep components small,Single responsibility principle,Split into smaller components,Large monolithic components,<Header /><Content /><Footer />,500+ line component,Medium,
3,Components,Use TypeScript,Type safety for props and state,TypeScript for new projects,JavaScript without types,const Button: FC<Props> = () => { },const Button = (props) => { },Medium, 3,Components,Use TypeScript,Type safety for props and state,TypeScript for new projects,JavaScript without types,const Button: FC<Props> = () => { },const Button = (props) => { },Medium,
4,Components,Colocate component files,Keep related files together,Component folder with styles,Flat structure,components/Button/index.tsx styles.ts,components/Button.tsx styles/button.ts,Low, 4,Components,Colocate component files,Keep related files together,Component folder with styles,Flat structure,components/Button/index.tsx styles.ts,components/Button.tsx styles/button.ts,Low,
5,Styling,Use StyleSheet.create,Optimized style objects,StyleSheet for all styles,Inline style objects,StyleSheet.create({ container: {} }),style={{ margin: 10 }},High,https://reactnative.dev/docs/stylesheet 5,Styling,Use StyleSheet.create,Optimized style objects,StyleSheet for all styles,Inline style objects,StyleSheet.create({ container: {} }),style={{ margin: 10 }},High,https://reactnative.dev/docs/stylesheet
6,Styling,Avoid inline styles,Prevent object recreation,Styles in StyleSheet,Inline style objects in render,style={styles.container},"style={{ margin: 10, padding: 5 }}",Medium, 6,Styling,Avoid inline styles,Prevent object recreation,Styles in StyleSheet,Inline style objects in render,style={styles.container},"style={{ margin: 10, padding: 5 }}",Medium,
7,Styling,Use flexbox for layout,React Native uses flexbox,flexDirection alignItems justifyContent,Absolute positioning everywhere,flexDirection: 'row',position: 'absolute' everywhere,Medium,https://reactnative.dev/docs/flexbox 7,Styling,Use flexbox for layout,React Native uses flexbox,flexDirection alignItems justifyContent,Absolute positioning everywhere,flexDirection: 'row',position: 'absolute' everywhere,Medium,https://reactnative.dev/docs/flexbox
8,Styling,Handle platform differences,Platform-specific styles,Platform.select or .ios/.android files,Same styles for both platforms,"Platform.select({ ios: {}, android: {} })",Hardcoded iOS values,Medium,https://reactnative.dev/docs/platform-specific-code 8,Styling,Handle platform differences,Platform-specific styles,Platform.select or .ios/.android files,Same styles for both platforms,"Platform.select({ ios: {}, android: {} })",Hardcoded iOS values,Medium,https://reactnative.dev/docs/platform-specific-code
9,Styling,Use responsive dimensions,Scale for different screens,Dimensions or useWindowDimensions,Fixed pixel values,useWindowDimensions(),width: 375,Medium, 9,Styling,Use responsive dimensions,Scale for different screens,Dimensions or useWindowDimensions,Fixed pixel values,useWindowDimensions(),width: 375,Medium,
10,Navigation,Use React Navigation,Standard navigation library,React Navigation for routing,Manual navigation management,createStackNavigator(),Custom navigation state,Medium,https://reactnavigation.org/ 10,Navigation,Use React Navigation,Standard navigation library,React Navigation for routing,Manual navigation management,createStackNavigator(),Custom navigation state,Medium,https://reactnavigation.org/
11,Navigation,Type navigation params,Type-safe navigation,Typed navigation props,Untyped navigation,"navigation.navigate<RootStackParamList>('Home', { id })","navigation.navigate('Home', { id })",Medium, 11,Navigation,Type navigation params,Type-safe navigation,Typed navigation props,Untyped navigation,"navigation.navigate<RootStackParamList>('Home', { id })","navigation.navigate('Home', { id })",Medium,
12,Navigation,Use deep linking,Support URL-based navigation,Configure linking prop,No deep link support,linking: { prefixes: [] },No linking configuration,Medium,https://reactnavigation.org/docs/deep-linking/ 12,Navigation,Use deep linking,Support URL-based navigation,Configure linking prop,No deep link support,linking: { prefixes: [] },No linking configuration,Medium,https://reactnavigation.org/docs/deep-linking/
13,Navigation,Handle back button,Android back button handling,useFocusEffect with BackHandler,Ignore back button,BackHandler.addEventListener,No back handler,High, 13,Navigation,Handle back button,Android back button handling,useFocusEffect with BackHandler,Ignore back button,BackHandler.addEventListener,No back handler,High,
14,State,Use useState for local state,Simple component state,useState for UI state,Class component state,"const [count, setCount] = useState(0)",this.state = { count: 0 },Medium, 14,State,Use useState for local state,Simple component state,useState for UI state,Class component state,"const [count, setCount] = useState(0)",this.state = { count: 0 },Medium,
15,State,Use useReducer for complex state,Complex state logic,useReducer for related state,Multiple useState for related values,useReducer(reducer initialState),5+ useState calls,Medium, 15,State,Use useReducer for complex state,Complex state logic,useReducer for related state,Multiple useState for related values,useReducer(reducer initialState),5+ useState calls,Medium,
16,State,Use context sparingly,Context for global state,Context for theme auth locale,Context for frequently changing data,ThemeContext for app theme,Context for list item data,Medium, 16,State,Use context sparingly,Context for global state,Context for theme auth locale,Context for frequently changing data,ThemeContext for app theme,Context for list item data,Medium,
17,State,Consider Zustand or Redux,External state management,Zustand for simple Redux for complex,useState for global state,create((set) => ({ })),Prop drilling global state,Medium, 17,State,Consider Zustand or Redux,External state management,Zustand for simple Redux for complex,useState for global state,create((set) => ({ })),Prop drilling global state,Medium,
18,Lists,Use FlatList for long lists,Virtualized list rendering,FlatList for 50+ items,ScrollView with map,<FlatList data={items} />,<ScrollView>{items.map()}</ScrollView>,High,https://reactnative.dev/docs/flatlist 18,Lists,Use FlatList for long lists,Virtualized list rendering,FlatList for 50+ items,ScrollView with map,<FlatList data={items} />,<ScrollView>{items.map()}</ScrollView>,High,https://reactnative.dev/docs/flatlist
19,Lists,Provide keyExtractor,Unique keys for list items,keyExtractor with stable ID,Index as key,keyExtractor={(item) => item.id},"keyExtractor={(_, index) => index}",High, 19,Lists,Provide keyExtractor,Unique keys for list items,keyExtractor with stable ID,Index as key,keyExtractor={(item) => item.id},"keyExtractor={(_, index) => index}",High,
20,Lists,Optimize renderItem,Memoize list item components,React.memo for list items,Inline render function,renderItem={({ item }) => <MemoizedItem item={item} />},renderItem={({ item }) => <View>...</View>},High, 20,Lists,Optimize renderItem,Memoize list item components,React.memo for list items,Inline render function,renderItem={({ item }) => <MemoizedItem item={item} />},renderItem={({ item }) => <View>...</View>},High,
21,Lists,Use getItemLayout for fixed height,Skip measurement for performance,getItemLayout when height known,Dynamic measurement for fixed items,"getItemLayout={(_, index) => ({ length: 50, offset: 50 * index, index })}",No getItemLayout for fixed height,Medium, 21,Lists,Use getItemLayout for fixed height,Skip measurement for performance,getItemLayout when height known,Dynamic measurement for fixed items,"getItemLayout={(_, index) => ({ length: 50, offset: 50 * index, index })}",No getItemLayout for fixed height,Medium,
22,Lists,Implement windowSize,Control render window,Smaller windowSize for memory,Default windowSize for large lists,windowSize={5},windowSize={21} for huge lists,Medium, 22,Lists,Implement windowSize,Control render window,Smaller windowSize for memory,Default windowSize for large lists,windowSize={5},windowSize={21} for huge lists,Medium,
23,Performance,Use React.memo,Prevent unnecessary re-renders,memo for pure components,No memoization,export default memo(MyComponent),export default MyComponent,Medium, 23,Performance,Use React.memo,Prevent unnecessary re-renders,memo for pure components,No memoization,export default memo(MyComponent),export default MyComponent,Medium,
24,Performance,Use useCallback for handlers,Stable function references,useCallback for props,New function on every render,"useCallback(() => {}, [deps])",() => handlePress(),Medium, 24,Performance,Use useCallback for handlers,Stable function references,useCallback for props,New function on every render,"useCallback(() => {}, [deps])",() => handlePress(),Medium,
25,Performance,Use useMemo for expensive ops,Cache expensive calculations,useMemo for heavy computations,Recalculate every render,"useMemo(() => expensive(), [deps])",const result = expensive(),Medium, 25,Performance,Use useMemo for expensive ops,Cache expensive calculations,useMemo for heavy computations,Recalculate every render,"useMemo(() => expensive(), [deps])",const result = expensive(),Medium,
26,Performance,Avoid anonymous functions in JSX,Prevent re-renders,Named handlers or useCallback,Inline arrow functions,onPress={handlePress},onPress={() => doSomething()},Medium, 26,Performance,Avoid anonymous functions in JSX,Prevent re-renders,Named handlers or useCallback,Inline arrow functions,onPress={handlePress},onPress={() => doSomething()},Medium,
27,Performance,Use Hermes engine,Improved startup and memory,Enable Hermes in build,JavaScriptCore for new projects,hermes_enabled: true,hermes_enabled: false,Medium,https://reactnative.dev/docs/hermes 27,Performance,Use Hermes engine,Improved startup and memory,Enable Hermes in build,JavaScriptCore for new projects,hermes_enabled: true,hermes_enabled: false,Medium,https://reactnative.dev/docs/hermes
28,Images,Use expo-image,Modern performant image component for React Native,"Use expo-image for caching, blurring, and performance",Use default Image for heavy lists or unmaintained libraries,<Image source={url} cachePolicy='memory-disk' /> (expo-image),<FastImage source={url} />,Medium,https://docs.expo.dev/versions/latest/sdk/image/ 28,Images,Use expo-image,Modern performant image component for React Native,"Use expo-image for caching, blurring, and performance",Use default Image for heavy lists or unmaintained libraries,<Image source={url} cachePolicy='memory-disk' /> (expo-image),<FastImage source={url} />,Medium,https://docs.expo.dev/versions/latest/sdk/image/
29,Images,Specify image dimensions,Prevent layout shifts,width and height for remote images,No dimensions for network images,<Image style={{ width: 100 height: 100 }} />,<Image source={{ uri }} /> no size,High, 29,Images,Specify image dimensions,Prevent layout shifts,width and height for remote images,No dimensions for network images,<Image style={{ width: 100 height: 100 }} />,<Image source={{ uri }} /> no size,High,
30,Images,Use resizeMode,Control image scaling,resizeMode cover contain,Stretch images,"resizeMode=""cover""",No resizeMode,Low, 30,Images,Use resizeMode,Control image scaling,resizeMode cover contain,Stretch images,"resizeMode=""cover""",No resizeMode,Low,
31,Forms,Use controlled inputs,State-controlled form fields,value + onChangeText,Uncontrolled inputs,<TextInput value={text} onChangeText={setText} />,<TextInput defaultValue={text} />,Medium, 31,Forms,Use controlled inputs,State-controlled form fields,value + onChangeText,Uncontrolled inputs,<TextInput value={text} onChangeText={setText} />,<TextInput defaultValue={text} />,Medium,
32,Forms,Handle keyboard,Manage keyboard visibility,KeyboardAvoidingView,Content hidden by keyboard,"<KeyboardAvoidingView behavior=""padding"">",No keyboard handling,High,https://reactnative.dev/docs/keyboardavoidingview 32,Forms,Handle keyboard,Manage keyboard visibility,KeyboardAvoidingView,Content hidden by keyboard,"<KeyboardAvoidingView behavior=""padding"">",No keyboard handling,High,https://reactnative.dev/docs/keyboardavoidingview
33,Forms,Use proper keyboard types,Appropriate keyboard for input,keyboardType for input type,Default keyboard for all,"keyboardType=""email-address""","keyboardType=""default"" for email",Low, 33,Forms,Use proper keyboard types,Appropriate keyboard for input,keyboardType for input type,Default keyboard for all,"keyboardType=""email-address""","keyboardType=""default"" for email",Low,
34,Touch,Use Pressable,Modern touch handling,Pressable for touch interactions,TouchableOpacity for new code,<Pressable onPress={} />,<TouchableOpacity onPress={} />,Low,https://reactnative.dev/docs/pressable 34,Touch,Use Pressable,Modern touch handling,Pressable for touch interactions,TouchableOpacity for new code,<Pressable onPress={} />,<TouchableOpacity onPress={} />,Low,https://reactnative.dev/docs/pressable
35,Touch,Provide touch feedback,Visual feedback on press,Ripple or opacity change,No feedback on press,android_ripple={{ color: 'gray' }},No press feedback,Medium, 35,Touch,Provide touch feedback,Visual feedback on press,Ripple or opacity change,No feedback on press,android_ripple={{ color: 'gray' }},No press feedback,Medium,
36,Touch,Set hitSlop for small targets,Increase touch area,hitSlop for icons and small buttons,Tiny touch targets,hitSlop={{ top: 10 bottom: 10 }},44x44 with no hitSlop,Medium, 36,Touch,Set hitSlop for small targets,Increase touch area,hitSlop for icons and small buttons,Tiny touch targets,hitSlop={{ top: 10 bottom: 10 }},44x44 with no hitSlop,Medium,
37,Animation,Use Reanimated,High-performance animations,react-native-reanimated,Animated API for complex,useSharedValue useAnimatedStyle,Animated.timing for gesture,Medium,https://docs.swmansion.com/react-native-reanimated/ 37,Animation,Use Reanimated,High-performance animations,react-native-reanimated,Animated API for complex,useSharedValue useAnimatedStyle,Animated.timing for gesture,Medium,https://docs.swmansion.com/react-native-reanimated/
38,Animation,Run on UI thread,worklets for smooth animation,Run animations on UI thread,JS thread animations,runOnUI(() => {}),Animated on JS thread,High, 38,Animation,Run on UI thread,worklets for smooth animation,Run animations on UI thread,JS thread animations,runOnUI(() => {}),Animated on JS thread,High,
39,Animation,Use gesture handler,Native gesture recognition,react-native-gesture-handler,JS-based gesture handling,<GestureDetector>,<View onTouchMove={} />,Medium,https://docs.swmansion.com/react-native-gesture-handler/ 39,Animation,Use gesture handler,Native gesture recognition,react-native-gesture-handler,JS-based gesture handling,<GestureDetector>,<View onTouchMove={} />,Medium,https://docs.swmansion.com/react-native-gesture-handler/
40,Async,Handle loading states,Show loading indicators,ActivityIndicator during load,Empty screen during load,{isLoading ? <ActivityIndicator /> : <Content />},No loading state,Medium, 40,Async,Handle loading states,Show loading indicators,ActivityIndicator during load,Empty screen during load,{isLoading ? <ActivityIndicator /> : <Content />},No loading state,Medium,
41,Async,Handle errors gracefully,Error boundaries and fallbacks,Error UI for failed requests,Crash on error,{error ? <ErrorView /> : <Content />},No error handling,High, 41,Async,Handle errors gracefully,Error boundaries and fallbacks,Error UI for failed requests,Crash on error,{error ? <ErrorView /> : <Content />},No error handling,High,
42,Async,Cancel async operations,Cleanup on unmount,AbortController or cleanup,Memory leaks from async,useEffect cleanup,No cleanup for subscriptions,High, 42,Async,Cancel async operations,Cleanup on unmount,AbortController or cleanup,Memory leaks from async,useEffect cleanup,No cleanup for subscriptions,High,
43,Accessibility,Add accessibility labels,Describe UI elements,accessibilityLabel for all interactive,Missing labels,"accessibilityLabel=""Submit form""",<Pressable> without label,High,https://reactnative.dev/docs/accessibility 43,Accessibility,Add accessibility labels,Describe UI elements,accessibilityLabel for all interactive,Missing labels,"accessibilityLabel=""Submit form""",<Pressable> without label,High,https://reactnative.dev/docs/accessibility
44,Accessibility,Use accessibility roles,Semantic meaning,accessibilityRole for elements,Wrong roles,"accessibilityRole=""button""",No role for button,Medium, 44,Accessibility,Use accessibility roles,Semantic meaning,accessibilityRole for elements,Wrong roles,"accessibilityRole=""button""",No role for button,Medium,
45,Accessibility,Support screen readers,Test with TalkBack/VoiceOver,Test with screen readers,Skip accessibility testing,Regular TalkBack testing,No screen reader testing,High, 45,Accessibility,Support screen readers,Test with TalkBack/VoiceOver,Test with screen readers,Skip accessibility testing,Regular TalkBack testing,No screen reader testing,High,
46,Testing,Use React Native Testing Library,Component testing,render and fireEvent,Enzyme or manual testing,render(<Component />),shallow(<Component />),Medium,https://callstack.github.io/react-native-testing-library/ 46,Testing,Use React Native Testing Library,Component testing,render and fireEvent,Enzyme or manual testing,render(<Component />),shallow(<Component />),Medium,https://callstack.github.io/react-native-testing-library/
47,Testing,Test on real devices,Real device behavior,Test on iOS and Android devices,Simulator only,Device testing in CI,Simulator only testing,High, 47,Testing,Test on real devices,Real device behavior,Test on iOS and Android devices,Simulator only,Device testing in CI,Simulator only testing,High,
48,Testing,Use Detox for E2E,End-to-end testing,Detox for critical flows,Manual E2E testing,detox test,Manual testing only,Medium,https://wix.github.io/Detox/ 48,Testing,Use Detox for E2E,End-to-end testing,Detox for critical flows,Manual E2E testing,detox test,Manual testing only,Medium,https://wix.github.io/Detox/
49,Native,Use native modules carefully,Bridge has overhead,Batch native calls,Frequent bridge crossing,Batch updates,Call native on every keystroke,High, 49,Native,Use native modules carefully,Bridge has overhead,Batch native calls,Frequent bridge crossing,Batch updates,Call native on every keystroke,High,
50,Native,Use Expo when possible,Simplified development,Expo for standard features,Bare RN for simple apps,expo install package,react-native link package,Low,https://docs.expo.dev/ 50,Native,Use Expo when possible,Simplified development,Expo for standard features,Bare RN for simple apps,expo install package,react-native link package,Low,https://docs.expo.dev/
51,Native,Handle permissions,Request permissions properly,Check and request permissions,Assume permissions granted,PermissionsAndroid.request(),Access without permission check,High,https://reactnative.dev/docs/permissionsandroid 51,Native,Handle permissions,Request permissions properly,Check and request permissions,Assume permissions granted,PermissionsAndroid.request(),Access without permission check,High,https://reactnative.dev/docs/permissionsandroid
1 No Category Guideline Description Do Don't Code Good Code Bad Severity Docs URL
2 1 Components Use functional components Hooks-based components are standard Functional components with hooks Class components const App = () => { } class App extends Component Medium https://reactnative.dev/docs/intro-react
3 2 Components Keep components small Single responsibility principle Split into smaller components Large monolithic components <Header /><Content /><Footer /> 500+ line component Medium
4 3 Components Use TypeScript Type safety for props and state TypeScript for new projects JavaScript without types const Button: FC<Props> = () => { } const Button = (props) => { } Medium
5 4 Components Colocate component files Keep related files together Component folder with styles Flat structure components/Button/index.tsx styles.ts components/Button.tsx styles/button.ts Low
6 5 Styling Use StyleSheet.create Optimized style objects StyleSheet for all styles Inline style objects StyleSheet.create({ container: {} }) style={{ margin: 10 }} High https://reactnative.dev/docs/stylesheet
7 6 Styling Avoid inline styles Prevent object recreation Styles in StyleSheet Inline style objects in render style={styles.container} style={{ margin: 10, padding: 5 }} Medium
8 7 Styling Use flexbox for layout React Native uses flexbox flexDirection alignItems justifyContent Absolute positioning everywhere flexDirection: 'row' position: 'absolute' everywhere Medium https://reactnative.dev/docs/flexbox
9 8 Styling Handle platform differences Platform-specific styles Platform.select or .ios/.android files Same styles for both platforms Platform.select({ ios: {}, android: {} }) Hardcoded iOS values Medium https://reactnative.dev/docs/platform-specific-code
10 9 Styling Use responsive dimensions Scale for different screens Dimensions or useWindowDimensions Fixed pixel values useWindowDimensions() width: 375 Medium
11 10 Navigation Use React Navigation Standard navigation library React Navigation for routing Manual navigation management createStackNavigator() Custom navigation state Medium https://reactnavigation.org/
12 11 Navigation Type navigation params Type-safe navigation Typed navigation props Untyped navigation navigation.navigate<RootStackParamList>('Home', { id }) navigation.navigate('Home', { id }) Medium
13 12 Navigation Use deep linking Support URL-based navigation Configure linking prop No deep link support linking: { prefixes: [] } No linking configuration Medium https://reactnavigation.org/docs/deep-linking/
14 13 Navigation Handle back button Android back button handling useFocusEffect with BackHandler Ignore back button BackHandler.addEventListener No back handler High
15 14 State Use useState for local state Simple component state useState for UI state Class component state const [count, setCount] = useState(0) this.state = { count: 0 } Medium
16 15 State Use useReducer for complex state Complex state logic useReducer for related state Multiple useState for related values useReducer(reducer initialState) 5+ useState calls Medium
17 16 State Use context sparingly Context for global state Context for theme auth locale Context for frequently changing data ThemeContext for app theme Context for list item data Medium
18 17 State Consider Zustand or Redux External state management Zustand for simple Redux for complex useState for global state create((set) => ({ })) Prop drilling global state Medium
19 18 Lists Use FlatList for long lists Virtualized list rendering FlatList for 50+ items ScrollView with map <FlatList data={items} /> <ScrollView>{items.map()}</ScrollView> High https://reactnative.dev/docs/flatlist
20 19 Lists Provide keyExtractor Unique keys for list items keyExtractor with stable ID Index as key keyExtractor={(item) => item.id} keyExtractor={(_, index) => index} High
21 20 Lists Optimize renderItem Memoize list item components React.memo for list items Inline render function renderItem={({ item }) => <MemoizedItem item={item} />} renderItem={({ item }) => <View>...</View>} High
22 21 Lists Use getItemLayout for fixed height Skip measurement for performance getItemLayout when height known Dynamic measurement for fixed items getItemLayout={(_, index) => ({ length: 50, offset: 50 * index, index })} No getItemLayout for fixed height Medium
23 22 Lists Implement windowSize Control render window Smaller windowSize for memory Default windowSize for large lists windowSize={5} windowSize={21} for huge lists Medium
24 23 Performance Use React.memo Prevent unnecessary re-renders memo for pure components No memoization export default memo(MyComponent) export default MyComponent Medium
25 24 Performance Use useCallback for handlers Stable function references useCallback for props New function on every render useCallback(() => {}, [deps]) () => handlePress() Medium
26 25 Performance Use useMemo for expensive ops Cache expensive calculations useMemo for heavy computations Recalculate every render useMemo(() => expensive(), [deps]) const result = expensive() Medium
27 26 Performance Avoid anonymous functions in JSX Prevent re-renders Named handlers or useCallback Inline arrow functions onPress={handlePress} onPress={() => doSomething()} Medium
28 27 Performance Use Hermes engine Improved startup and memory Enable Hermes in build JavaScriptCore for new projects hermes_enabled: true hermes_enabled: false Medium https://reactnative.dev/docs/hermes
29 28 Images Use expo-image Modern performant image component for React Native Use expo-image for caching, blurring, and performance Use default Image for heavy lists or unmaintained libraries <Image source={url} cachePolicy='memory-disk' /> (expo-image) <FastImage source={url} /> Medium https://docs.expo.dev/versions/latest/sdk/image/
30 29 Images Specify image dimensions Prevent layout shifts width and height for remote images No dimensions for network images <Image style={{ width: 100 height: 100 }} /> <Image source={{ uri }} /> no size High
31 30 Images Use resizeMode Control image scaling resizeMode cover contain Stretch images resizeMode="cover" No resizeMode Low
32 31 Forms Use controlled inputs State-controlled form fields value + onChangeText Uncontrolled inputs <TextInput value={text} onChangeText={setText} /> <TextInput defaultValue={text} /> Medium
33 32 Forms Handle keyboard Manage keyboard visibility KeyboardAvoidingView Content hidden by keyboard <KeyboardAvoidingView behavior="padding"> No keyboard handling High https://reactnative.dev/docs/keyboardavoidingview
34 33 Forms Use proper keyboard types Appropriate keyboard for input keyboardType for input type Default keyboard for all keyboardType="email-address" keyboardType="default" for email Low
35 34 Touch Use Pressable Modern touch handling Pressable for touch interactions TouchableOpacity for new code <Pressable onPress={} /> <TouchableOpacity onPress={} /> Low https://reactnative.dev/docs/pressable
36 35 Touch Provide touch feedback Visual feedback on press Ripple or opacity change No feedback on press android_ripple={{ color: 'gray' }} No press feedback Medium
37 36 Touch Set hitSlop for small targets Increase touch area hitSlop for icons and small buttons Tiny touch targets hitSlop={{ top: 10 bottom: 10 }} 44x44 with no hitSlop Medium
38 37 Animation Use Reanimated High-performance animations react-native-reanimated Animated API for complex useSharedValue useAnimatedStyle Animated.timing for gesture Medium https://docs.swmansion.com/react-native-reanimated/
39 38 Animation Run on UI thread worklets for smooth animation Run animations on UI thread JS thread animations runOnUI(() => {}) Animated on JS thread High
40 39 Animation Use gesture handler Native gesture recognition react-native-gesture-handler JS-based gesture handling <GestureDetector> <View onTouchMove={} /> Medium https://docs.swmansion.com/react-native-gesture-handler/
41 40 Async Handle loading states Show loading indicators ActivityIndicator during load Empty screen during load {isLoading ? <ActivityIndicator /> : <Content />} No loading state Medium
42 41 Async Handle errors gracefully Error boundaries and fallbacks Error UI for failed requests Crash on error {error ? <ErrorView /> : <Content />} No error handling High
43 42 Async Cancel async operations Cleanup on unmount AbortController or cleanup Memory leaks from async useEffect cleanup No cleanup for subscriptions High
44 43 Accessibility Add accessibility labels Describe UI elements accessibilityLabel for all interactive Missing labels accessibilityLabel="Submit form" <Pressable> without label High https://reactnative.dev/docs/accessibility
45 44 Accessibility Use accessibility roles Semantic meaning accessibilityRole for elements Wrong roles accessibilityRole="button" No role for button Medium
46 45 Accessibility Support screen readers Test with TalkBack/VoiceOver Test with screen readers Skip accessibility testing Regular TalkBack testing No screen reader testing High
47 46 Testing Use React Native Testing Library Component testing render and fireEvent Enzyme or manual testing render(<Component />) shallow(<Component />) Medium https://callstack.github.io/react-native-testing-library/
48 47 Testing Test on real devices Real device behavior Test on iOS and Android devices Simulator only Device testing in CI Simulator only testing High
49 48 Testing Use Detox for E2E End-to-end testing Detox for critical flows Manual E2E testing detox test Manual testing only Medium https://wix.github.io/Detox/
50 49 Native Use native modules carefully Bridge has overhead Batch native calls Frequent bridge crossing Batch updates Call native on every keystroke High
51 50 Native Use Expo when possible Simplified development Expo for standard features Bare RN for simple apps expo install package react-native link package Low https://docs.expo.dev/
52 51 Native Handle permissions Request permissions properly Check and request permissions Assume permissions granted PermissionsAndroid.request() Access without permission check High https://reactnative.dev/docs/permissionsandroid

View File

@@ -1,54 +1,54 @@
No,Category,Guideline,Description,Do,Don't,Code Good,Code Bad,Severity,Docs URL No,Category,Guideline,Description,Do,Don't,Code Good,Code Bad,Severity,Docs URL
1,State,Use useState for local state,Simple component state should use useState hook,useState for form inputs toggles counters,Class components this.state,"const [count, setCount] = useState(0)",this.state = { count: 0 },Medium,https://react.dev/reference/react/useState 1,State,Use useState for local state,Simple component state should use useState hook,useState for form inputs toggles counters,Class components this.state,"const [count, setCount] = useState(0)",this.state = { count: 0 },Medium,https://react.dev/reference/react/useState
2,State,Lift state up when needed,Share state between siblings by lifting to parent,Lift shared state to common ancestor,Prop drilling through many levels,Parent holds state passes down,Deep prop chains,Medium,https://react.dev/learn/sharing-state-between-components 2,State,Lift state up when needed,Share state between siblings by lifting to parent,Lift shared state to common ancestor,Prop drilling through many levels,Parent holds state passes down,Deep prop chains,Medium,https://react.dev/learn/sharing-state-between-components
3,State,Use useReducer for complex state,Complex state logic benefits from reducer pattern,useReducer for state with multiple sub-values,Multiple useState for related values,useReducer with action types,5+ useState calls that update together,Medium,https://react.dev/reference/react/useReducer 3,State,Use useReducer for complex state,Complex state logic benefits from reducer pattern,useReducer for state with multiple sub-values,Multiple useState for related values,useReducer with action types,5+ useState calls that update together,Medium,https://react.dev/reference/react/useReducer
4,State,Avoid unnecessary state,Derive values from existing state when possible,Compute derived values in render,Store derivable values in state,const total = items.reduce(...),"const [total, setTotal] = useState(0)",High,https://react.dev/learn/choosing-the-state-structure 4,State,Avoid unnecessary state,Derive values from existing state when possible,Compute derived values in render,Store derivable values in state,const total = items.reduce(...),"const [total, setTotal] = useState(0)",High,https://react.dev/learn/choosing-the-state-structure
5,State,Initialize state lazily,Use function form for expensive initial state,useState(() => computeExpensive()),useState(computeExpensive()),useState(() => JSON.parse(data)),useState(JSON.parse(data)),Medium,https://react.dev/reference/react/useState#avoiding-recreating-the-initial-state 5,State,Initialize state lazily,Use function form for expensive initial state,useState(() => computeExpensive()),useState(computeExpensive()),useState(() => JSON.parse(data)),useState(JSON.parse(data)),Medium,https://react.dev/reference/react/useState#avoiding-recreating-the-initial-state
6,Effects,Clean up effects,Return cleanup function for subscriptions timers,Return cleanup function in useEffect,No cleanup for subscriptions,useEffect(() => { sub(); return unsub; }),useEffect(() => { subscribe(); }),High,https://react.dev/reference/react/useEffect#connecting-to-an-external-system 6,Effects,Clean up effects,Return cleanup function for subscriptions timers,Return cleanup function in useEffect,No cleanup for subscriptions,useEffect(() => { sub(); return unsub; }),useEffect(() => { subscribe(); }),High,https://react.dev/reference/react/useEffect#connecting-to-an-external-system
7,Effects,Specify dependencies correctly,Include all values used inside effect in deps array,All referenced values in dependency array,Empty deps with external references,[value] when using value in effect,[] when using props/state in effect,High,https://react.dev/reference/react/useEffect#specifying-reactive-dependencies 7,Effects,Specify dependencies correctly,Include all values used inside effect in deps array,All referenced values in dependency array,Empty deps with external references,[value] when using value in effect,[] when using props/state in effect,High,https://react.dev/reference/react/useEffect#specifying-reactive-dependencies
8,Effects,Avoid unnecessary effects,Don't use effects for transforming data or events,Transform data during render handle events directly,useEffect for derived state or event handling,const filtered = items.filter(...),useEffect(() => setFiltered(items.filter(...))),High,https://react.dev/learn/you-might-not-need-an-effect 8,Effects,Avoid unnecessary effects,Don't use effects for transforming data or events,Transform data during render handle events directly,useEffect for derived state or event handling,const filtered = items.filter(...),useEffect(() => setFiltered(items.filter(...))),High,https://react.dev/learn/you-might-not-need-an-effect
9,Effects,Use refs for non-reactive values,Store values that don't trigger re-renders in refs,useRef for interval IDs DOM elements,useState for values that don't need render,const intervalRef = useRef(null),"const [intervalId, setIntervalId] = useState()",Medium,https://react.dev/reference/react/useRef 9,Effects,Use refs for non-reactive values,Store values that don't trigger re-renders in refs,useRef for interval IDs DOM elements,useState for values that don't need render,const intervalRef = useRef(null),"const [intervalId, setIntervalId] = useState()",Medium,https://react.dev/reference/react/useRef
10,Rendering,Use keys properly,Stable unique keys for list items,Use stable IDs as keys,Array index as key for dynamic lists,key={item.id},key={index},High,https://react.dev/learn/rendering-lists#keeping-list-items-in-order-with-key 10,Rendering,Use keys properly,Stable unique keys for list items,Use stable IDs as keys,Array index as key for dynamic lists,key={item.id},key={index},High,https://react.dev/learn/rendering-lists#keeping-list-items-in-order-with-key
11,Rendering,Memoize expensive calculations,Use useMemo for costly computations,useMemo for expensive filtering/sorting,Recalculate every render,"useMemo(() => expensive(), [deps])",const result = expensiveCalc(),Medium,https://react.dev/reference/react/useMemo 11,Rendering,Memoize expensive calculations,Use useMemo for costly computations,useMemo for expensive filtering/sorting,Recalculate every render,"useMemo(() => expensive(), [deps])",const result = expensiveCalc(),Medium,https://react.dev/reference/react/useMemo
12,Rendering,Memoize callbacks passed to children,Use useCallback for functions passed as props,useCallback for handlers passed to memoized children,New function reference every render,"useCallback(() => {}, [deps])",const handler = () => {},Medium,https://react.dev/reference/react/useCallback 12,Rendering,Memoize callbacks passed to children,Use useCallback for functions passed as props,useCallback for handlers passed to memoized children,New function reference every render,"useCallback(() => {}, [deps])",const handler = () => {},Medium,https://react.dev/reference/react/useCallback
13,Rendering,Use React.memo wisely,Wrap components that render often with same props,memo for pure components with stable props,memo everything or nothing,memo(ExpensiveList),memo(SimpleButton),Low,https://react.dev/reference/react/memo 13,Rendering,Use React.memo wisely,Wrap components that render often with same props,memo for pure components with stable props,memo everything or nothing,memo(ExpensiveList),memo(SimpleButton),Low,https://react.dev/reference/react/memo
14,Rendering,Avoid inline object/array creation in JSX,Create objects outside render or memoize,Define style objects outside component,Inline objects in props,<div style={styles.container}>,<div style={{ margin: 10 }}>,Medium, 14,Rendering,Avoid inline object/array creation in JSX,Create objects outside render or memoize,Define style objects outside component,Inline objects in props,<div style={styles.container}>,<div style={{ margin: 10 }}>,Medium,
15,Components,Keep components small and focused,Single responsibility for each component,One concern per component,Large multi-purpose components,<UserAvatar /><UserName />,<UserCard /> with 500 lines,Medium, 15,Components,Keep components small and focused,Single responsibility for each component,One concern per component,Large multi-purpose components,<UserAvatar /><UserName />,<UserCard /> with 500 lines,Medium,
16,Components,Use composition over inheritance,Compose components using children and props,Use children prop for flexibility,Inheritance hierarchies,<Card>{content}</Card>,class SpecialCard extends Card,Medium,https://react.dev/learn/thinking-in-react 16,Components,Use composition over inheritance,Compose components using children and props,Use children prop for flexibility,Inheritance hierarchies,<Card>{content}</Card>,class SpecialCard extends Card,Medium,https://react.dev/learn/thinking-in-react
17,Components,Colocate related code,Keep related components and hooks together,Related files in same directory,Flat structure with many files,components/User/UserCard.tsx,components/UserCard.tsx + hooks/useUser.ts,Low, 17,Components,Colocate related code,Keep related components and hooks together,Related files in same directory,Flat structure with many files,components/User/UserCard.tsx,components/UserCard.tsx + hooks/useUser.ts,Low,
18,Components,Use fragments to avoid extra DOM,Fragment or <> for multiple elements without wrapper,<> for grouping without DOM node,Extra div wrappers,<>{items.map(...)}</>,<div>{items.map(...)}</div>,Low,https://react.dev/reference/react/Fragment 18,Components,Use fragments to avoid extra DOM,Fragment or <> for multiple elements without wrapper,<> for grouping without DOM node,Extra div wrappers,<>{items.map(...)}</>,<div>{items.map(...)}</div>,Low,https://react.dev/reference/react/Fragment
19,Props,Destructure props,Destructure props for cleaner component code,Destructure in function signature,props.name props.value throughout,"function User({ name, age })",function User(props),Low, 19,Props,Destructure props,Destructure props for cleaner component code,Destructure in function signature,props.name props.value throughout,"function User({ name, age })",function User(props),Low,
20,Props,Provide default props values,Use default parameters or defaultProps,Default values in destructuring,Undefined checks throughout,function Button({ size = 'md' }),if (size === undefined) size = 'md',Low, 20,Props,Provide default props values,Use default parameters or defaultProps,Default values in destructuring,Undefined checks throughout,function Button({ size = 'md' }),if (size === undefined) size = 'md',Low,
21,Props,Avoid prop drilling,Use context or composition for deeply nested data,Context for global data composition for UI,Passing props through 5+ levels,<UserContext.Provider>,<A user={u}><B user={u}><C user={u}>,Medium,https://react.dev/learn/passing-data-deeply-with-context 21,Props,Avoid prop drilling,Use context or composition for deeply nested data,Context for global data composition for UI,Passing props through 5+ levels,<UserContext.Provider>,<A user={u}><B user={u}><C user={u}>,Medium,https://react.dev/learn/passing-data-deeply-with-context
22,Props,Validate props with TypeScript,Use TypeScript interfaces for prop types,interface Props { name: string },PropTypes or no validation,interface ButtonProps { onClick: () => void },Button.propTypes = {},Medium, 22,Props,Validate props with TypeScript,Use TypeScript interfaces for prop types,interface Props { name: string },PropTypes or no validation,interface ButtonProps { onClick: () => void },Button.propTypes = {},Medium,
23,Events,Use synthetic events correctly,React normalizes events across browsers,e.preventDefault() e.stopPropagation(),Access native event unnecessarily,onClick={(e) => e.preventDefault()},onClick={(e) => e.nativeEvent.preventDefault()},Low,https://react.dev/reference/react-dom/components/common#react-event-object 23,Events,Use synthetic events correctly,React normalizes events across browsers,e.preventDefault() e.stopPropagation(),Access native event unnecessarily,onClick={(e) => e.preventDefault()},onClick={(e) => e.nativeEvent.preventDefault()},Low,https://react.dev/reference/react-dom/components/common#react-event-object
24,Events,Avoid binding in render,Use arrow functions in class or hooks,Arrow functions in functional components,bind in render or constructor,const handleClick = () => {},this.handleClick.bind(this),Medium, 24,Events,Avoid binding in render,Use arrow functions in class or hooks,Arrow functions in functional components,bind in render or constructor,const handleClick = () => {},this.handleClick.bind(this),Medium,
25,Events,Pass event handlers not call results,Pass function reference not invocation,onClick={handleClick},onClick={handleClick()} causing immediate call,onClick={handleClick},onClick={handleClick()},High, 25,Events,Pass event handlers not call results,Pass function reference not invocation,onClick={handleClick},onClick={handleClick()} causing immediate call,onClick={handleClick},onClick={handleClick()},High,
26,Forms,Controlled components for forms,Use state to control form inputs,value + onChange for inputs,Uncontrolled inputs with refs,<input value={val} onChange={setVal}>,<input ref={inputRef}>,Medium,https://react.dev/reference/react-dom/components/input#controlling-an-input-with-a-state-variable 26,Forms,Controlled components for forms,Use state to control form inputs,value + onChange for inputs,Uncontrolled inputs with refs,<input value={val} onChange={setVal}>,<input ref={inputRef}>,Medium,https://react.dev/reference/react-dom/components/input#controlling-an-input-with-a-state-variable
27,Forms,Handle form submission properly,Prevent default and handle in submit handler,onSubmit with preventDefault,onClick on submit button only,<form onSubmit={handleSubmit}>,<button onClick={handleSubmit}>,Medium, 27,Forms,Handle form submission properly,Prevent default and handle in submit handler,onSubmit with preventDefault,onClick on submit button only,<form onSubmit={handleSubmit}>,<button onClick={handleSubmit}>,Medium,
28,Forms,Debounce rapid input changes,Debounce search/filter inputs,useDeferredValue or debounce for search,Filter on every keystroke,useDeferredValue(searchTerm),useEffect filtering on every change,Medium,https://react.dev/reference/react/useDeferredValue 28,Forms,Debounce rapid input changes,Debounce search/filter inputs,useDeferredValue or debounce for search,Filter on every keystroke,useDeferredValue(searchTerm),useEffect filtering on every change,Medium,https://react.dev/reference/react/useDeferredValue
29,Hooks,Follow rules of hooks,Only call hooks at top level and in React functions,Hooks at component top level,Hooks in conditions loops or callbacks,"const [x, setX] = useState()","if (cond) { const [x, setX] = useState() }",High,https://react.dev/reference/rules/rules-of-hooks 29,Hooks,Follow rules of hooks,Only call hooks at top level and in React functions,Hooks at component top level,Hooks in conditions loops or callbacks,"const [x, setX] = useState()","if (cond) { const [x, setX] = useState() }",High,https://react.dev/reference/rules/rules-of-hooks
30,Hooks,Custom hooks for reusable logic,Extract shared stateful logic to custom hooks,useCustomHook for reusable patterns,Duplicate hook logic across components,const { data } = useFetch(url),Duplicate useEffect/useState in components,Medium,https://react.dev/learn/reusing-logic-with-custom-hooks 30,Hooks,Custom hooks for reusable logic,Extract shared stateful logic to custom hooks,useCustomHook for reusable patterns,Duplicate hook logic across components,const { data } = useFetch(url),Duplicate useEffect/useState in components,Medium,https://react.dev/learn/reusing-logic-with-custom-hooks
31,Hooks,Name custom hooks with use prefix,Custom hooks must start with use,useFetch useForm useAuth,fetchData or getData for hook,function useFetch(url),function fetchData(url),High, 31,Hooks,Name custom hooks with use prefix,Custom hooks must start with use,useFetch useForm useAuth,fetchData or getData for hook,function useFetch(url),function fetchData(url),High,
32,Context,Use context for global data,Context for theme auth locale,Context for app-wide state,Context for frequently changing data,<ThemeContext.Provider>,Context for form field values,Medium,https://react.dev/learn/passing-data-deeply-with-context 32,Context,Use context for global data,Context for theme auth locale,Context for app-wide state,Context for frequently changing data,<ThemeContext.Provider>,Context for form field values,Medium,https://react.dev/learn/passing-data-deeply-with-context
33,Context,Split contexts by concern,Separate contexts for different domains,ThemeContext + AuthContext,One giant AppContext,<ThemeProvider><AuthProvider>,<AppProvider value={{theme user...}}>,Medium, 33,Context,Split contexts by concern,Separate contexts for different domains,ThemeContext + AuthContext,One giant AppContext,<ThemeProvider><AuthProvider>,<AppProvider value={{theme user...}}>,Medium,
34,Context,Memoize context values,Prevent unnecessary re-renders with useMemo,useMemo for context value object,New object reference every render,"value={useMemo(() => ({...}), [])}","value={{ user, theme }}",High, 34,Context,Memoize context values,Prevent unnecessary re-renders with useMemo,useMemo for context value object,New object reference every render,"value={useMemo(() => ({...}), [])}","value={{ user, theme }}",High,
35,Performance,Use React DevTools Profiler,Profile to identify performance bottlenecks,Profile before optimizing,Optimize without measuring,React DevTools Profiler,Guessing at bottlenecks,Medium,https://react.dev/learn/react-developer-tools 35,Performance,Use React DevTools Profiler,Profile to identify performance bottlenecks,Profile before optimizing,Optimize without measuring,React DevTools Profiler,Guessing at bottlenecks,Medium,https://react.dev/learn/react-developer-tools
36,Performance,Lazy load components,Use React.lazy for code splitting,lazy() for routes and heavy components,Import everything upfront,const Page = lazy(() => import('./Page')),import Page from './Page',Medium,https://react.dev/reference/react/lazy 36,Performance,Lazy load components,Use React.lazy for code splitting,lazy() for routes and heavy components,Import everything upfront,const Page = lazy(() => import('./Page')),import Page from './Page',Medium,https://react.dev/reference/react/lazy
37,Performance,Virtualize long lists,Use windowing for lists over 100 items,react-window or react-virtual,Render thousands of DOM nodes,<VirtualizedList items={items}/>,{items.map(i => <Item />)},High, 37,Performance,Virtualize long lists,Use windowing for lists over 100 items,react-window or react-virtual,Render thousands of DOM nodes,<VirtualizedList items={items}/>,{items.map(i => <Item />)},High,
38,Performance,Batch state updates,React 18 auto-batches but be aware,Let React batch related updates,Manual batching with flushSync,setA(1); setB(2); // batched,flushSync(() => setA(1)),Low,https://react.dev/learn/queueing-a-series-of-state-updates 38,Performance,Batch state updates,React 18 auto-batches but be aware,Let React batch related updates,Manual batching with flushSync,setA(1); setB(2); // batched,flushSync(() => setA(1)),Low,https://react.dev/learn/queueing-a-series-of-state-updates
39,ErrorHandling,Use error boundaries,Catch JavaScript errors in component tree,ErrorBoundary wrapping sections,Let errors crash entire app,<ErrorBoundary><App/></ErrorBoundary>,No error handling,High,https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary 39,ErrorHandling,Use error boundaries,Catch JavaScript errors in component tree,ErrorBoundary wrapping sections,Let errors crash entire app,<ErrorBoundary><App/></ErrorBoundary>,No error handling,High,https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary
40,ErrorHandling,Handle async errors,Catch errors in async operations,try/catch in async handlers,Unhandled promise rejections,try { await fetch() } catch(e) {},await fetch() // no catch,High, 40,ErrorHandling,Handle async errors,Catch errors in async operations,try/catch in async handlers,Unhandled promise rejections,try { await fetch() } catch(e) {},await fetch() // no catch,High,
41,Testing,Test behavior not implementation,Test what user sees and does,Test renders and interactions,Test internal state or methods,expect(screen.getByText('Hello')),expect(component.state.name),Medium,https://testing-library.com/docs/react-testing-library/intro/ 41,Testing,Test behavior not implementation,Test what user sees and does,Test renders and interactions,Test internal state or methods,expect(screen.getByText('Hello')),expect(component.state.name),Medium,https://testing-library.com/docs/react-testing-library/intro/
42,Testing,Use testing-library queries,Use accessible queries,getByRole getByLabelText,getByTestId for everything,getByRole('button'),getByTestId('submit-btn'),Medium,https://testing-library.com/docs/queries/about#priority 42,Testing,Use testing-library queries,Use accessible queries,getByRole getByLabelText,getByTestId for everything,getByRole('button'),getByTestId('submit-btn'),Medium,https://testing-library.com/docs/queries/about#priority
43,Accessibility,Use semantic HTML,Proper HTML elements for their purpose,button for clicks nav for navigation,div with onClick for buttons,<button onClick={...}>,<div onClick={...}>,High,https://react.dev/reference/react-dom/components#all-html-components 43,Accessibility,Use semantic HTML,Proper HTML elements for their purpose,button for clicks nav for navigation,div with onClick for buttons,<button onClick={...}>,<div onClick={...}>,High,https://react.dev/reference/react-dom/components#all-html-components
44,Accessibility,Manage focus properly,Handle focus for modals dialogs,Focus trap in modals return focus on close,No focus management,useEffect to focus input,Modal without focus trap,High, 44,Accessibility,Manage focus properly,Handle focus for modals dialogs,Focus trap in modals return focus on close,No focus management,useEffect to focus input,Modal without focus trap,High,
45,Accessibility,Announce dynamic content,Use ARIA live regions for updates,aria-live for dynamic updates,Silent updates to screen readers,"<div aria-live=""polite"">{msg}</div>",<div>{msg}</div>,Medium, 45,Accessibility,Announce dynamic content,Use ARIA live regions for updates,aria-live for dynamic updates,Silent updates to screen readers,"<div aria-live=""polite"">{msg}</div>",<div>{msg}</div>,Medium,
46,Accessibility,Label form controls,Associate labels with inputs,htmlFor matching input id,Placeholder as only label,"<label htmlFor=""email"">Email</label>","<input placeholder=""Email""/>",High, 46,Accessibility,Label form controls,Associate labels with inputs,htmlFor matching input id,Placeholder as only label,"<label htmlFor=""email"">Email</label>","<input placeholder=""Email""/>",High,
47,TypeScript,Type component props,Define interfaces for all props,interface Props with all prop types,any or missing types,interface Props { name: string },function Component(props: any),High, 47,TypeScript,Type component props,Define interfaces for all props,interface Props with all prop types,any or missing types,interface Props { name: string },function Component(props: any),High,
48,TypeScript,Type state properly,Provide types for useState,useState<Type>() for complex state,Inferred any types,useState<User | null>(null),useState(null),Medium, 48,TypeScript,Type state properly,Provide types for useState,useState<Type>() for complex state,Inferred any types,useState<User | null>(null),useState(null),Medium,
49,TypeScript,Type event handlers,Use React event types,React.ChangeEvent<HTMLInputElement>,Generic Event type,onChange: React.ChangeEvent<HTMLInputElement>,onChange: Event,Medium, 49,TypeScript,Type event handlers,Use React event types,React.ChangeEvent<HTMLInputElement>,Generic Event type,onChange: React.ChangeEvent<HTMLInputElement>,onChange: Event,Medium,
50,TypeScript,Use generics for reusable components,Generic components for flexible typing,Generic props for list components,Union types for flexibility,<List<T> items={T[]}>,<List items={any[]}>,Medium, 50,TypeScript,Use generics for reusable components,Generic components for flexible typing,Generic props for list components,Union types for flexibility,<List<T> items={T[]}>,<List items={any[]}>,Medium,
51,Patterns,Container/Presentational split,Separate data logic from UI,Container fetches presentational renders,Mixed data and UI in one,<UserContainer><UserView/></UserContainer>,<User /> with fetch and render,Low, 51,Patterns,Container/Presentational split,Separate data logic from UI,Container fetches presentational renders,Mixed data and UI in one,<UserContainer><UserView/></UserContainer>,<User /> with fetch and render,Low,
52,Patterns,Render props for flexibility,Share code via render prop pattern,Render prop for customizable rendering,Duplicate logic across components,<DataFetcher render={data => ...}/>,Copy paste fetch logic,Low,https://react.dev/reference/react/cloneElement#passing-data-with-a-render-prop 52,Patterns,Render props for flexibility,Share code via render prop pattern,Render prop for customizable rendering,Duplicate logic across components,<DataFetcher render={data => ...}/>,Copy paste fetch logic,Low,https://react.dev/reference/react/cloneElement#passing-data-with-a-render-prop
53,Patterns,Compound components,Related components sharing state,Tab + TabPanel sharing context,Prop drilling between related,<Tabs><Tab/><TabPanel/></Tabs>,<Tabs tabs={[]} panels={[...]}/>,Low, 53,Patterns,Compound components,Related components sharing state,Tab + TabPanel sharing context,Prop drilling between related,<Tabs><Tab/><TabPanel/></Tabs>,<Tabs tabs={[]} panels={[...]}/>,Low,
1 No Category Guideline Description Do Don't Code Good Code Bad Severity Docs URL
2 1 State Use useState for local state Simple component state should use useState hook useState for form inputs toggles counters Class components this.state const [count, setCount] = useState(0) this.state = { count: 0 } Medium https://react.dev/reference/react/useState
3 2 State Lift state up when needed Share state between siblings by lifting to parent Lift shared state to common ancestor Prop drilling through many levels Parent holds state passes down Deep prop chains Medium https://react.dev/learn/sharing-state-between-components
4 3 State Use useReducer for complex state Complex state logic benefits from reducer pattern useReducer for state with multiple sub-values Multiple useState for related values useReducer with action types 5+ useState calls that update together Medium https://react.dev/reference/react/useReducer
5 4 State Avoid unnecessary state Derive values from existing state when possible Compute derived values in render Store derivable values in state const total = items.reduce(...) const [total, setTotal] = useState(0) High https://react.dev/learn/choosing-the-state-structure
6 5 State Initialize state lazily Use function form for expensive initial state useState(() => computeExpensive()) useState(computeExpensive()) useState(() => JSON.parse(data)) useState(JSON.parse(data)) Medium https://react.dev/reference/react/useState#avoiding-recreating-the-initial-state
7 6 Effects Clean up effects Return cleanup function for subscriptions timers Return cleanup function in useEffect No cleanup for subscriptions useEffect(() => { sub(); return unsub; }) useEffect(() => { subscribe(); }) High https://react.dev/reference/react/useEffect#connecting-to-an-external-system
8 7 Effects Specify dependencies correctly Include all values used inside effect in deps array All referenced values in dependency array Empty deps with external references [value] when using value in effect [] when using props/state in effect High https://react.dev/reference/react/useEffect#specifying-reactive-dependencies
9 8 Effects Avoid unnecessary effects Don't use effects for transforming data or events Transform data during render handle events directly useEffect for derived state or event handling const filtered = items.filter(...) useEffect(() => setFiltered(items.filter(...))) High https://react.dev/learn/you-might-not-need-an-effect
10 9 Effects Use refs for non-reactive values Store values that don't trigger re-renders in refs useRef for interval IDs DOM elements useState for values that don't need render const intervalRef = useRef(null) const [intervalId, setIntervalId] = useState() Medium https://react.dev/reference/react/useRef
11 10 Rendering Use keys properly Stable unique keys for list items Use stable IDs as keys Array index as key for dynamic lists key={item.id} key={index} High https://react.dev/learn/rendering-lists#keeping-list-items-in-order-with-key
12 11 Rendering Memoize expensive calculations Use useMemo for costly computations useMemo for expensive filtering/sorting Recalculate every render useMemo(() => expensive(), [deps]) const result = expensiveCalc() Medium https://react.dev/reference/react/useMemo
13 12 Rendering Memoize callbacks passed to children Use useCallback for functions passed as props useCallback for handlers passed to memoized children New function reference every render useCallback(() => {}, [deps]) const handler = () => {} Medium https://react.dev/reference/react/useCallback
14 13 Rendering Use React.memo wisely Wrap components that render often with same props memo for pure components with stable props memo everything or nothing memo(ExpensiveList) memo(SimpleButton) Low https://react.dev/reference/react/memo
15 14 Rendering Avoid inline object/array creation in JSX Create objects outside render or memoize Define style objects outside component Inline objects in props <div style={styles.container}> <div style={{ margin: 10 }}> Medium
16 15 Components Keep components small and focused Single responsibility for each component One concern per component Large multi-purpose components <UserAvatar /><UserName /> <UserCard /> with 500 lines Medium
17 16 Components Use composition over inheritance Compose components using children and props Use children prop for flexibility Inheritance hierarchies <Card>{content}</Card> class SpecialCard extends Card Medium https://react.dev/learn/thinking-in-react
18 17 Components Colocate related code Keep related components and hooks together Related files in same directory Flat structure with many files components/User/UserCard.tsx components/UserCard.tsx + hooks/useUser.ts Low
19 18 Components Use fragments to avoid extra DOM Fragment or <> for multiple elements without wrapper <> for grouping without DOM node Extra div wrappers <>{items.map(...)}</> <div>{items.map(...)}</div> Low https://react.dev/reference/react/Fragment
20 19 Props Destructure props Destructure props for cleaner component code Destructure in function signature props.name props.value throughout function User({ name, age }) function User(props) Low
21 20 Props Provide default props values Use default parameters or defaultProps Default values in destructuring Undefined checks throughout function Button({ size = 'md' }) if (size === undefined) size = 'md' Low
22 21 Props Avoid prop drilling Use context or composition for deeply nested data Context for global data composition for UI Passing props through 5+ levels <UserContext.Provider> <A user={u}><B user={u}><C user={u}> Medium https://react.dev/learn/passing-data-deeply-with-context
23 22 Props Validate props with TypeScript Use TypeScript interfaces for prop types interface Props { name: string } PropTypes or no validation interface ButtonProps { onClick: () => void } Button.propTypes = {} Medium
24 23 Events Use synthetic events correctly React normalizes events across browsers e.preventDefault() e.stopPropagation() Access native event unnecessarily onClick={(e) => e.preventDefault()} onClick={(e) => e.nativeEvent.preventDefault()} Low https://react.dev/reference/react-dom/components/common#react-event-object
25 24 Events Avoid binding in render Use arrow functions in class or hooks Arrow functions in functional components bind in render or constructor const handleClick = () => {} this.handleClick.bind(this) Medium
26 25 Events Pass event handlers not call results Pass function reference not invocation onClick={handleClick} onClick={handleClick()} causing immediate call onClick={handleClick} onClick={handleClick()} High
27 26 Forms Controlled components for forms Use state to control form inputs value + onChange for inputs Uncontrolled inputs with refs <input value={val} onChange={setVal}> <input ref={inputRef}> Medium https://react.dev/reference/react-dom/components/input#controlling-an-input-with-a-state-variable
28 27 Forms Handle form submission properly Prevent default and handle in submit handler onSubmit with preventDefault onClick on submit button only <form onSubmit={handleSubmit}> <button onClick={handleSubmit}> Medium
29 28 Forms Debounce rapid input changes Debounce search/filter inputs useDeferredValue or debounce for search Filter on every keystroke useDeferredValue(searchTerm) useEffect filtering on every change Medium https://react.dev/reference/react/useDeferredValue
30 29 Hooks Follow rules of hooks Only call hooks at top level and in React functions Hooks at component top level Hooks in conditions loops or callbacks const [x, setX] = useState() if (cond) { const [x, setX] = useState() } High https://react.dev/reference/rules/rules-of-hooks
31 30 Hooks Custom hooks for reusable logic Extract shared stateful logic to custom hooks useCustomHook for reusable patterns Duplicate hook logic across components const { data } = useFetch(url) Duplicate useEffect/useState in components Medium https://react.dev/learn/reusing-logic-with-custom-hooks
32 31 Hooks Name custom hooks with use prefix Custom hooks must start with use useFetch useForm useAuth fetchData or getData for hook function useFetch(url) function fetchData(url) High
33 32 Context Use context for global data Context for theme auth locale Context for app-wide state Context for frequently changing data <ThemeContext.Provider> Context for form field values Medium https://react.dev/learn/passing-data-deeply-with-context
34 33 Context Split contexts by concern Separate contexts for different domains ThemeContext + AuthContext One giant AppContext <ThemeProvider><AuthProvider> <AppProvider value={{theme user...}}> Medium
35 34 Context Memoize context values Prevent unnecessary re-renders with useMemo useMemo for context value object New object reference every render value={useMemo(() => ({...}), [])} value={{ user, theme }} High
36 35 Performance Use React DevTools Profiler Profile to identify performance bottlenecks Profile before optimizing Optimize without measuring React DevTools Profiler Guessing at bottlenecks Medium https://react.dev/learn/react-developer-tools
37 36 Performance Lazy load components Use React.lazy for code splitting lazy() for routes and heavy components Import everything upfront const Page = lazy(() => import('./Page')) import Page from './Page' Medium https://react.dev/reference/react/lazy
38 37 Performance Virtualize long lists Use windowing for lists over 100 items react-window or react-virtual Render thousands of DOM nodes <VirtualizedList items={items}/> {items.map(i => <Item />)} High
39 38 Performance Batch state updates React 18 auto-batches but be aware Let React batch related updates Manual batching with flushSync setA(1); setB(2); // batched flushSync(() => setA(1)) Low https://react.dev/learn/queueing-a-series-of-state-updates
40 39 ErrorHandling Use error boundaries Catch JavaScript errors in component tree ErrorBoundary wrapping sections Let errors crash entire app <ErrorBoundary><App/></ErrorBoundary> No error handling High https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary
41 40 ErrorHandling Handle async errors Catch errors in async operations try/catch in async handlers Unhandled promise rejections try { await fetch() } catch(e) {} await fetch() // no catch High
42 41 Testing Test behavior not implementation Test what user sees and does Test renders and interactions Test internal state or methods expect(screen.getByText('Hello')) expect(component.state.name) Medium https://testing-library.com/docs/react-testing-library/intro/
43 42 Testing Use testing-library queries Use accessible queries getByRole getByLabelText getByTestId for everything getByRole('button') getByTestId('submit-btn') Medium https://testing-library.com/docs/queries/about#priority
44 43 Accessibility Use semantic HTML Proper HTML elements for their purpose button for clicks nav for navigation div with onClick for buttons <button onClick={...}> <div onClick={...}> High https://react.dev/reference/react-dom/components#all-html-components
45 44 Accessibility Manage focus properly Handle focus for modals dialogs Focus trap in modals return focus on close No focus management useEffect to focus input Modal without focus trap High
46 45 Accessibility Announce dynamic content Use ARIA live regions for updates aria-live for dynamic updates Silent updates to screen readers <div aria-live="polite">{msg}</div> <div>{msg}</div> Medium
47 46 Accessibility Label form controls Associate labels with inputs htmlFor matching input id Placeholder as only label <label htmlFor="email">Email</label> <input placeholder="Email"/> High
48 47 TypeScript Type component props Define interfaces for all props interface Props with all prop types any or missing types interface Props { name: string } function Component(props: any) High
49 48 TypeScript Type state properly Provide types for useState useState<Type>() for complex state Inferred any types useState<User | null>(null) useState(null) Medium
50 49 TypeScript Type event handlers Use React event types React.ChangeEvent<HTMLInputElement> Generic Event type onChange: React.ChangeEvent<HTMLInputElement> onChange: Event Medium
51 50 TypeScript Use generics for reusable components Generic components for flexible typing Generic props for list components Union types for flexibility <List<T> items={T[]}> <List items={any[]}> Medium
52 51 Patterns Container/Presentational split Separate data logic from UI Container fetches presentational renders Mixed data and UI in one <UserContainer><UserView/></UserContainer> <User /> with fetch and render Low
53 52 Patterns Render props for flexibility Share code via render prop pattern Render prop for customizable rendering Duplicate logic across components <DataFetcher render={data => ...}/> Copy paste fetch logic Low https://react.dev/reference/react/cloneElement#passing-data-with-a-render-prop
54 53 Patterns Compound components Related components sharing state Tab + TabPanel sharing context Prop drilling between related <Tabs><Tab/><TabPanel/></Tabs> <Tabs tabs={[]} panels={[...]}/> Low

View File

@@ -1,54 +1,54 @@
No,Category,Guideline,Description,Do,Don't,Code Good,Code Bad,Severity,Docs URL No,Category,Guideline,Description,Do,Don't,Code Good,Code Bad,Severity,Docs URL
1,Reactivity,Use $: for reactive statements,Automatic dependency tracking,$: for derived values,Manual recalculation,$: doubled = count * 2,let doubled; count && (doubled = count * 2),Medium,https://svelte.dev/docs/svelte-components#script-3-$-marks-a-statement-as-reactive 1,Reactivity,Use $: for reactive statements,Automatic dependency tracking,$: for derived values,Manual recalculation,$: doubled = count * 2,let doubled; count && (doubled = count * 2),Medium,https://svelte.dev/docs/svelte-components#script-3-$-marks-a-statement-as-reactive
2,Reactivity,Trigger reactivity with assignment,Svelte tracks assignments not mutations,Reassign arrays/objects to trigger update,Mutate without reassignment,"items = [...items, newItem]",items.push(newItem),High,https://svelte.dev/docs/svelte-components#script-2-assignments-are-reactive 2,Reactivity,Trigger reactivity with assignment,Svelte tracks assignments not mutations,Reassign arrays/objects to trigger update,Mutate without reassignment,"items = [...items, newItem]",items.push(newItem),High,https://svelte.dev/docs/svelte-components#script-2-assignments-are-reactive
3,Reactivity,Use $state in Svelte 5,Runes for explicit reactivity,let count = $state(0),Implicit reactivity in Svelte 5,let count = $state(0),let count = 0 (Svelte 5),Medium,https://svelte.dev/blog/runes 3,Reactivity,Use $state in Svelte 5,Runes for explicit reactivity,let count = $state(0),Implicit reactivity in Svelte 5,let count = $state(0),let count = 0 (Svelte 5),Medium,https://svelte.dev/blog/runes
4,Reactivity,Use $derived for computed values,$derived replaces $: in Svelte 5,let doubled = $derived(count * 2),$: in Svelte 5,let doubled = $derived(count * 2),$: doubled = count * 2 (Svelte 5),Medium, 4,Reactivity,Use $derived for computed values,$derived replaces $: in Svelte 5,let doubled = $derived(count * 2),$: in Svelte 5,let doubled = $derived(count * 2),$: doubled = count * 2 (Svelte 5),Medium,
5,Reactivity,Use $effect for side effects,$effect replaces $: side effects,Use $effect for subscriptions,$: for side effects in Svelte 5,$effect(() => console.log(count)),$: console.log(count) (Svelte 5),Medium, 5,Reactivity,Use $effect for side effects,$effect replaces $: side effects,Use $effect for subscriptions,$: for side effects in Svelte 5,$effect(() => console.log(count)),$: console.log(count) (Svelte 5),Medium,
6,Props,Export let for props,Declare props with export let,export let propName,Props without export,export let count = 0,let count = 0,High,https://svelte.dev/docs/svelte-components#script-1-export-creates-a-component-prop 6,Props,Export let for props,Declare props with export let,export let propName,Props without export,export let count = 0,let count = 0,High,https://svelte.dev/docs/svelte-components#script-1-export-creates-a-component-prop
7,Props,Use $props in Svelte 5,$props rune for prop access,let { name } = $props(),export let in Svelte 5,"let { name, age = 0 } = $props()",export let name; export let age = 0,Medium, 7,Props,Use $props in Svelte 5,$props rune for prop access,let { name } = $props(),export let in Svelte 5,"let { name, age = 0 } = $props()",export let name; export let age = 0,Medium,
8,Props,Provide default values,Default props with assignment,export let count = 0,Required props without defaults,export let count = 0,export let count,Low, 8,Props,Provide default values,Default props with assignment,export let count = 0,Required props without defaults,export let count = 0,export let count,Low,
9,Props,Use spread props,Pass through unknown props,{...$$restProps} on elements,Manual prop forwarding,<button {...$$restProps}>,<button class={$$props.class}>,Low,https://svelte.dev/docs/basic-markup#attributes-and-props 9,Props,Use spread props,Pass through unknown props,{...$$restProps} on elements,Manual prop forwarding,<button {...$$restProps}>,<button class={$$props.class}>,Low,https://svelte.dev/docs/basic-markup#attributes-and-props
10,Bindings,Use bind: for two-way binding,Simplified input handling,bind:value for inputs,on:input with manual update,<input bind:value={name}>,<input value={name} on:input={e => name = e.target.value}>,Low,https://svelte.dev/docs/element-directives#bind-property 10,Bindings,Use bind: for two-way binding,Simplified input handling,bind:value for inputs,on:input with manual update,<input bind:value={name}>,<input value={name} on:input={e => name = e.target.value}>,Low,https://svelte.dev/docs/element-directives#bind-property
11,Bindings,Bind to DOM elements,Reference DOM nodes,bind:this for element reference,querySelector in onMount,<div bind:this={el}>,onMount(() => el = document.querySelector()),Medium, 11,Bindings,Bind to DOM elements,Reference DOM nodes,bind:this for element reference,querySelector in onMount,<div bind:this={el}>,onMount(() => el = document.querySelector()),Medium,
12,Bindings,Use bind:group for radios/checkboxes,Simplified group handling,bind:group for radio/checkbox groups,Manual checked handling,"<input type=""radio"" bind:group={selected}>","<input type=""radio"" checked={selected === value}>",Low, 12,Bindings,Use bind:group for radios/checkboxes,Simplified group handling,bind:group for radio/checkbox groups,Manual checked handling,"<input type=""radio"" bind:group={selected}>","<input type=""radio"" checked={selected === value}>",Low,
13,Events,Use on: for event handlers,Event directive syntax,on:click={handler},addEventListener in onMount,<button on:click={handleClick}>,onMount(() => btn.addEventListener()),Medium,https://svelte.dev/docs/element-directives#on-eventname 13,Events,Use on: for event handlers,Event directive syntax,on:click={handler},addEventListener in onMount,<button on:click={handleClick}>,onMount(() => btn.addEventListener()),Medium,https://svelte.dev/docs/element-directives#on-eventname
14,Events,Forward events with on:event,Pass events to parent,on:click without handler,createEventDispatcher for DOM events,<button on:click>,"dispatch('click', event)",Low, 14,Events,Forward events with on:event,Pass events to parent,on:click without handler,createEventDispatcher for DOM events,<button on:click>,"dispatch('click', event)",Low,
15,Events,Use createEventDispatcher,Custom component events,dispatch for custom events,on:event for custom events,"dispatch('save', { data })",on:save without dispatch,Medium,https://svelte.dev/docs/svelte#createeventdispatcher 15,Events,Use createEventDispatcher,Custom component events,dispatch for custom events,on:event for custom events,"dispatch('save', { data })",on:save without dispatch,Medium,https://svelte.dev/docs/svelte#createeventdispatcher
16,Lifecycle,Use onMount for initialization,Run code after component mounts,onMount for setup and data fetching,Code in script body for side effects,onMount(() => fetchData()),fetchData() in script body,High,https://svelte.dev/docs/svelte#onmount 16,Lifecycle,Use onMount for initialization,Run code after component mounts,onMount for setup and data fetching,Code in script body for side effects,onMount(() => fetchData()),fetchData() in script body,High,https://svelte.dev/docs/svelte#onmount
17,Lifecycle,Return cleanup from onMount,Automatic cleanup on destroy,Return function from onMount,Separate onDestroy for paired cleanup,onMount(() => { sub(); return unsub }),onMount(sub); onDestroy(unsub),Medium, 17,Lifecycle,Return cleanup from onMount,Automatic cleanup on destroy,Return function from onMount,Separate onDestroy for paired cleanup,onMount(() => { sub(); return unsub }),onMount(sub); onDestroy(unsub),Medium,
18,Lifecycle,Use onDestroy sparingly,Only when onMount cleanup not possible,onDestroy for non-mount cleanup,onDestroy for mount-related cleanup,onDestroy for store unsubscribe,onDestroy(() => clearInterval(id)),Low, 18,Lifecycle,Use onDestroy sparingly,Only when onMount cleanup not possible,onDestroy for non-mount cleanup,onDestroy for mount-related cleanup,onDestroy for store unsubscribe,onDestroy(() => clearInterval(id)),Low,
19,Lifecycle,Avoid beforeUpdate/afterUpdate,Usually not needed,Reactive statements instead,beforeUpdate for derived state,$: if (x) doSomething(),beforeUpdate(() => doSomething()),Low, 19,Lifecycle,Avoid beforeUpdate/afterUpdate,Usually not needed,Reactive statements instead,beforeUpdate for derived state,$: if (x) doSomething(),beforeUpdate(() => doSomething()),Low,
20,Stores,Use writable for mutable state,Basic reactive store,writable for shared mutable state,Local variables for shared state,const count = writable(0),let count = 0 in module,Medium,https://svelte.dev/docs/svelte-store#writable 20,Stores,Use writable for mutable state,Basic reactive store,writable for shared mutable state,Local variables for shared state,const count = writable(0),let count = 0 in module,Medium,https://svelte.dev/docs/svelte-store#writable
21,Stores,Use readable for read-only state,External data sources,readable for derived/external data,writable for read-only data,"readable(0, set => interval(set))",writable(0) for timer,Low,https://svelte.dev/docs/svelte-store#readable 21,Stores,Use readable for read-only state,External data sources,readable for derived/external data,writable for read-only data,"readable(0, set => interval(set))",writable(0) for timer,Low,https://svelte.dev/docs/svelte-store#readable
22,Stores,Use derived for computed stores,Combine or transform stores,derived for computed values,Manual subscription for derived,"derived(count, $c => $c * 2)",count.subscribe(c => doubled = c * 2),Medium,https://svelte.dev/docs/svelte-store#derived 22,Stores,Use derived for computed stores,Combine or transform stores,derived for computed values,Manual subscription for derived,"derived(count, $c => $c * 2)",count.subscribe(c => doubled = c * 2),Medium,https://svelte.dev/docs/svelte-store#derived
23,Stores,Use $ prefix for auto-subscription,Automatic subscribe/unsubscribe,$storeName in components,Manual subscription,{$count},count.subscribe(c => value = c),High, 23,Stores,Use $ prefix for auto-subscription,Automatic subscribe/unsubscribe,$storeName in components,Manual subscription,{$count},count.subscribe(c => value = c),High,
24,Stores,Clean up custom subscriptions,Unsubscribe when component destroys,Return unsubscribe from onMount,Leave subscriptions open,onMount(() => store.subscribe(fn)),store.subscribe(fn) in script,High, 24,Stores,Clean up custom subscriptions,Unsubscribe when component destroys,Return unsubscribe from onMount,Leave subscriptions open,onMount(() => store.subscribe(fn)),store.subscribe(fn) in script,High,
25,Slots,Use slots for composition,Content projection,<slot> for flexible content,Props for all content,<slot>Default</slot>,"<Component content=""text""/>",Medium,https://svelte.dev/docs/special-elements#slot 25,Slots,Use slots for composition,Content projection,<slot> for flexible content,Props for all content,<slot>Default</slot>,"<Component content=""text""/>",Medium,https://svelte.dev/docs/special-elements#slot
26,Slots,Name slots for multiple areas,Multiple content areas,"<slot name=""header"">",Single slot for complex layouts,"<slot name=""header""><slot name=""footer"">",<slot> with complex conditionals,Low, 26,Slots,Name slots for multiple areas,Multiple content areas,"<slot name=""header"">",Single slot for complex layouts,"<slot name=""header""><slot name=""footer"">",<slot> with complex conditionals,Low,
27,Slots,Check slot content with $$slots,Conditional slot rendering,$$slots.name for conditional rendering,Always render slot wrapper,"{#if $$slots.footer}<slot name=""footer""/>{/if}","<div><slot name=""footer""/></div>",Low, 27,Slots,Check slot content with $$slots,Conditional slot rendering,$$slots.name for conditional rendering,Always render slot wrapper,"{#if $$slots.footer}<slot name=""footer""/>{/if}","<div><slot name=""footer""/></div>",Low,
28,Styling,Use scoped styles by default,Styles scoped to component,<style> for component styles,Global styles for component,:global() only when needed,<style> all global,Medium,https://svelte.dev/docs/svelte-components#style 28,Styling,Use scoped styles by default,Styles scoped to component,<style> for component styles,Global styles for component,:global() only when needed,<style> all global,Medium,https://svelte.dev/docs/svelte-components#style
29,Styling,Use :global() sparingly,Escape scoping when needed,:global for third-party styling,Global for all styles,:global(.external-lib),<style> without scoping,Medium, 29,Styling,Use :global() sparingly,Escape scoping when needed,:global for third-party styling,Global for all styles,:global(.external-lib),<style> without scoping,Medium,
30,Styling,Use CSS variables for theming,Dynamic styling,CSS custom properties,Inline styles for themes,"style=""--color: {color}""","style=""color: {color}""",Low, 30,Styling,Use CSS variables for theming,Dynamic styling,CSS custom properties,Inline styles for themes,"style=""--color: {color}""","style=""color: {color}""",Low,
31,Transitions,Use built-in transitions,Svelte transition directives,transition:fade for simple effects,Manual CSS transitions,<div transition:fade>,<div class:fade={visible}>,Low,https://svelte.dev/docs/element-directives#transition-fn 31,Transitions,Use built-in transitions,Svelte transition directives,transition:fade for simple effects,Manual CSS transitions,<div transition:fade>,<div class:fade={visible}>,Low,https://svelte.dev/docs/element-directives#transition-fn
32,Transitions,Use in: and out: separately,Different enter/exit animations,in:fly out:fade for asymmetric,Same transition for both,<div in:fly out:fade>,<div transition:fly>,Low, 32,Transitions,Use in: and out: separately,Different enter/exit animations,in:fly out:fade for asymmetric,Same transition for both,<div in:fly out:fade>,<div transition:fly>,Low,
33,Transitions,Add local modifier,Prevent ancestor trigger,transition:fade|local,Global transitions for lists,<div transition:slide|local>,<div transition:slide>,Medium, 33,Transitions,Add local modifier,Prevent ancestor trigger,transition:fade|local,Global transitions for lists,<div transition:slide|local>,<div transition:slide>,Medium,
34,Actions,Use actions for DOM behavior,Reusable DOM logic,use:action for DOM enhancements,onMount for each usage,<div use:clickOutside>,onMount(() => setupClickOutside(el)),Medium,https://svelte.dev/docs/element-directives#use-action 34,Actions,Use actions for DOM behavior,Reusable DOM logic,use:action for DOM enhancements,onMount for each usage,<div use:clickOutside>,onMount(() => setupClickOutside(el)),Medium,https://svelte.dev/docs/element-directives#use-action
35,Actions,Return update and destroy,Lifecycle methods for actions,"Return { update, destroy }",Only initial setup,"return { update(params) {}, destroy() {} }",return destroy only,Medium, 35,Actions,Return update and destroy,Lifecycle methods for actions,"Return { update, destroy }",Only initial setup,"return { update(params) {}, destroy() {} }",return destroy only,Medium,
36,Actions,Pass parameters to actions,Configure action behavior,use:action={params},Hardcoded action behavior,<div use:tooltip={options}>,<div use:tooltip>,Low, 36,Actions,Pass parameters to actions,Configure action behavior,use:action={params},Hardcoded action behavior,<div use:tooltip={options}>,<div use:tooltip>,Low,
37,Logic,Use {#if} for conditionals,Template conditionals,{#if} {:else if} {:else},Ternary in expressions,{#if cond}...{:else}...{/if},{cond ? a : b} for complex,Low,https://svelte.dev/docs/logic-blocks#if 37,Logic,Use {#if} for conditionals,Template conditionals,{#if} {:else if} {:else},Ternary in expressions,{#if cond}...{:else}...{/if},{cond ? a : b} for complex,Low,https://svelte.dev/docs/logic-blocks#if
38,Logic,Use {#each} for lists,List rendering,{#each} with key,Map in expression,{#each items as item (item.id)},{items.map(i => `<div>${i}</div>`)},Medium, 38,Logic,Use {#each} for lists,List rendering,{#each} with key,Map in expression,{#each items as item (item.id)},{items.map(i => `<div>${i}</div>`)},Medium,
39,Logic,Always use keys in {#each},Proper list reconciliation,(item.id) for unique key,Index as key or no key,{#each items as item (item.id)},"{#each items as item, i (i)}",High, 39,Logic,Always use keys in {#each},Proper list reconciliation,(item.id) for unique key,Index as key or no key,{#each items as item (item.id)},"{#each items as item, i (i)}",High,
40,Logic,Use {#await} for promises,Handle async states,{#await} for loading/error states,Manual promise handling,{#await promise}...{:then}...{:catch},{#if loading}...{#if error},Medium,https://svelte.dev/docs/logic-blocks#await 40,Logic,Use {#await} for promises,Handle async states,{#await} for loading/error states,Manual promise handling,{#await promise}...{:then}...{:catch},{#if loading}...{#if error},Medium,https://svelte.dev/docs/logic-blocks#await
41,SvelteKit,Use +page.svelte for routes,File-based routing,+page.svelte for route components,Custom routing setup,routes/about/+page.svelte,routes/About.svelte,Medium,https://kit.svelte.dev/docs/routing 41,SvelteKit,Use +page.svelte for routes,File-based routing,+page.svelte for route components,Custom routing setup,routes/about/+page.svelte,routes/About.svelte,Medium,https://kit.svelte.dev/docs/routing
42,SvelteKit,Use +page.js for data loading,Load data before render,load function in +page.js,onMount for data fetching,export function load() {},onMount(() => fetchData()),High,https://kit.svelte.dev/docs/load 42,SvelteKit,Use +page.js for data loading,Load data before render,load function in +page.js,onMount for data fetching,export function load() {},onMount(() => fetchData()),High,https://kit.svelte.dev/docs/load
43,SvelteKit,Use +page.server.js for server-only,Server-side data loading,+page.server.js for sensitive data,+page.js for API keys,+page.server.js with DB access,+page.js with DB access,High, 43,SvelteKit,Use +page.server.js for server-only,Server-side data loading,+page.server.js for sensitive data,+page.js for API keys,+page.server.js with DB access,+page.js with DB access,High,
44,SvelteKit,Use form actions,Server-side form handling,+page.server.js actions,API routes for forms,export const actions = { default },fetch('/api/submit'),Medium,https://kit.svelte.dev/docs/form-actions 44,SvelteKit,Use form actions,Server-side form handling,+page.server.js actions,API routes for forms,export const actions = { default },fetch('/api/submit'),Medium,https://kit.svelte.dev/docs/form-actions
45,SvelteKit,Use $app/stores for app state,$page $navigating $updated,$page for current page data,Manual URL parsing,import { page } from '$app/stores',window.location.pathname,Medium,https://kit.svelte.dev/docs/modules#$app-stores 45,SvelteKit,Use $app/stores for app state,$page $navigating $updated,$page for current page data,Manual URL parsing,import { page } from '$app/stores',window.location.pathname,Medium,https://kit.svelte.dev/docs/modules#$app-stores
46,Performance,Use {#key} for forced re-render,Reset component state,{#key id} for fresh instance,Manual destroy/create,{#key item.id}<Component/>{/key},on:change={() => component = null},Low,https://svelte.dev/docs/logic-blocks#key 46,Performance,Use {#key} for forced re-render,Reset component state,{#key id} for fresh instance,Manual destroy/create,{#key item.id}<Component/>{/key},on:change={() => component = null},Low,https://svelte.dev/docs/logic-blocks#key
47,Performance,Avoid unnecessary reactivity,Not everything needs $:,$: only for side effects,$: for simple assignments,$: if (x) console.log(x),$: y = x (when y = x works),Low, 47,Performance,Avoid unnecessary reactivity,Not everything needs $:,$: only for side effects,$: for simple assignments,$: if (x) console.log(x),$: y = x (when y = x works),Low,
48,Performance,Use immutable compiler option,Skip equality checks,immutable: true for large lists,Default for all components,<svelte:options immutable/>,Default without immutable,Low, 48,Performance,Use immutable compiler option,Skip equality checks,immutable: true for large lists,Default for all components,<svelte:options immutable/>,Default without immutable,Low,
49,TypeScript,"Use lang=""ts"" in script",TypeScript support,"<script lang=""ts"">",JavaScript for typed projects,"<script lang=""ts"">",<script> with JSDoc,Medium,https://svelte.dev/docs/typescript 49,TypeScript,"Use lang=""ts"" in script",TypeScript support,"<script lang=""ts"">",JavaScript for typed projects,"<script lang=""ts"">",<script> with JSDoc,Medium,https://svelte.dev/docs/typescript
50,TypeScript,Type props with interface,Explicit prop types,interface $$Props for types,Untyped props,interface $$Props { name: string },export let name,Medium, 50,TypeScript,Type props with interface,Explicit prop types,interface $$Props for types,Untyped props,interface $$Props { name: string },export let name,Medium,
51,TypeScript,Type events with createEventDispatcher,Type-safe events,createEventDispatcher<Events>(),Untyped dispatch,createEventDispatcher<{ save: Data }>(),createEventDispatcher(),Medium, 51,TypeScript,Type events with createEventDispatcher,Type-safe events,createEventDispatcher<Events>(),Untyped dispatch,createEventDispatcher<{ save: Data }>(),createEventDispatcher(),Medium,
52,Accessibility,Use semantic elements,Proper HTML in templates,button nav main appropriately,div for everything,<button on:click>,<div on:click>,High, 52,Accessibility,Use semantic elements,Proper HTML in templates,button nav main appropriately,div for everything,<button on:click>,<div on:click>,High,
53,Accessibility,Add aria to dynamic content,Accessible state changes,aria-live for updates,Silent dynamic updates,"<div aria-live=""polite"">{message}</div>",<div>{message}</div>,Medium, 53,Accessibility,Add aria to dynamic content,Accessible state changes,aria-live for updates,Silent dynamic updates,"<div aria-live=""polite"">{message}</div>",<div>{message}</div>,Medium,
1 No Category Guideline Description Do Don't Code Good Code Bad Severity Docs URL
2 1 Reactivity Use $: for reactive statements Automatic dependency tracking $: for derived values Manual recalculation $: doubled = count * 2 let doubled; count && (doubled = count * 2) Medium https://svelte.dev/docs/svelte-components#script-3-$-marks-a-statement-as-reactive
3 2 Reactivity Trigger reactivity with assignment Svelte tracks assignments not mutations Reassign arrays/objects to trigger update Mutate without reassignment items = [...items, newItem] items.push(newItem) High https://svelte.dev/docs/svelte-components#script-2-assignments-are-reactive
4 3 Reactivity Use $state in Svelte 5 Runes for explicit reactivity let count = $state(0) Implicit reactivity in Svelte 5 let count = $state(0) let count = 0 (Svelte 5) Medium https://svelte.dev/blog/runes
5 4 Reactivity Use $derived for computed values $derived replaces $: in Svelte 5 let doubled = $derived(count * 2) $: in Svelte 5 let doubled = $derived(count * 2) $: doubled = count * 2 (Svelte 5) Medium
6 5 Reactivity Use $effect for side effects $effect replaces $: side effects Use $effect for subscriptions $: for side effects in Svelte 5 $effect(() => console.log(count)) $: console.log(count) (Svelte 5) Medium
7 6 Props Export let for props Declare props with export let export let propName Props without export export let count = 0 let count = 0 High https://svelte.dev/docs/svelte-components#script-1-export-creates-a-component-prop
8 7 Props Use $props in Svelte 5 $props rune for prop access let { name } = $props() export let in Svelte 5 let { name, age = 0 } = $props() export let name; export let age = 0 Medium
9 8 Props Provide default values Default props with assignment export let count = 0 Required props without defaults export let count = 0 export let count Low
10 9 Props Use spread props Pass through unknown props {...$$restProps} on elements Manual prop forwarding <button {...$$restProps}> <button class={$$props.class}> Low https://svelte.dev/docs/basic-markup#attributes-and-props
11 10 Bindings Use bind: for two-way binding Simplified input handling bind:value for inputs on:input with manual update <input bind:value={name}> <input value={name} on:input={e => name = e.target.value}> Low https://svelte.dev/docs/element-directives#bind-property
12 11 Bindings Bind to DOM elements Reference DOM nodes bind:this for element reference querySelector in onMount <div bind:this={el}> onMount(() => el = document.querySelector()) Medium
13 12 Bindings Use bind:group for radios/checkboxes Simplified group handling bind:group for radio/checkbox groups Manual checked handling <input type="radio" bind:group={selected}> <input type="radio" checked={selected === value}> Low
14 13 Events Use on: for event handlers Event directive syntax on:click={handler} addEventListener in onMount <button on:click={handleClick}> onMount(() => btn.addEventListener()) Medium https://svelte.dev/docs/element-directives#on-eventname
15 14 Events Forward events with on:event Pass events to parent on:click without handler createEventDispatcher for DOM events <button on:click> dispatch('click', event) Low
16 15 Events Use createEventDispatcher Custom component events dispatch for custom events on:event for custom events dispatch('save', { data }) on:save without dispatch Medium https://svelte.dev/docs/svelte#createeventdispatcher
17 16 Lifecycle Use onMount for initialization Run code after component mounts onMount for setup and data fetching Code in script body for side effects onMount(() => fetchData()) fetchData() in script body High https://svelte.dev/docs/svelte#onmount
18 17 Lifecycle Return cleanup from onMount Automatic cleanup on destroy Return function from onMount Separate onDestroy for paired cleanup onMount(() => { sub(); return unsub }) onMount(sub); onDestroy(unsub) Medium
19 18 Lifecycle Use onDestroy sparingly Only when onMount cleanup not possible onDestroy for non-mount cleanup onDestroy for mount-related cleanup onDestroy for store unsubscribe onDestroy(() => clearInterval(id)) Low
20 19 Lifecycle Avoid beforeUpdate/afterUpdate Usually not needed Reactive statements instead beforeUpdate for derived state $: if (x) doSomething() beforeUpdate(() => doSomething()) Low
21 20 Stores Use writable for mutable state Basic reactive store writable for shared mutable state Local variables for shared state const count = writable(0) let count = 0 in module Medium https://svelte.dev/docs/svelte-store#writable
22 21 Stores Use readable for read-only state External data sources readable for derived/external data writable for read-only data readable(0, set => interval(set)) writable(0) for timer Low https://svelte.dev/docs/svelte-store#readable
23 22 Stores Use derived for computed stores Combine or transform stores derived for computed values Manual subscription for derived derived(count, $c => $c * 2) count.subscribe(c => doubled = c * 2) Medium https://svelte.dev/docs/svelte-store#derived
24 23 Stores Use $ prefix for auto-subscription Automatic subscribe/unsubscribe $storeName in components Manual subscription {$count} count.subscribe(c => value = c) High
25 24 Stores Clean up custom subscriptions Unsubscribe when component destroys Return unsubscribe from onMount Leave subscriptions open onMount(() => store.subscribe(fn)) store.subscribe(fn) in script High
26 25 Slots Use slots for composition Content projection <slot> for flexible content Props for all content <slot>Default</slot> <Component content="text"/> Medium https://svelte.dev/docs/special-elements#slot
27 26 Slots Name slots for multiple areas Multiple content areas <slot name="header"> Single slot for complex layouts <slot name="header"><slot name="footer"> <slot> with complex conditionals Low
28 27 Slots Check slot content with $$slots Conditional slot rendering $$slots.name for conditional rendering Always render slot wrapper {#if $$slots.footer}<slot name="footer"/>{/if} <div><slot name="footer"/></div> Low
29 28 Styling Use scoped styles by default Styles scoped to component <style> for component styles Global styles for component :global() only when needed <style> all global Medium https://svelte.dev/docs/svelte-components#style
30 29 Styling Use :global() sparingly Escape scoping when needed :global for third-party styling Global for all styles :global(.external-lib) <style> without scoping Medium
31 30 Styling Use CSS variables for theming Dynamic styling CSS custom properties Inline styles for themes style="--color: {color}" style="color: {color}" Low
32 31 Transitions Use built-in transitions Svelte transition directives transition:fade for simple effects Manual CSS transitions <div transition:fade> <div class:fade={visible}> Low https://svelte.dev/docs/element-directives#transition-fn
33 32 Transitions Use in: and out: separately Different enter/exit animations in:fly out:fade for asymmetric Same transition for both <div in:fly out:fade> <div transition:fly> Low
34 33 Transitions Add local modifier Prevent ancestor trigger transition:fade|local Global transitions for lists <div transition:slide|local> <div transition:slide> Medium
35 34 Actions Use actions for DOM behavior Reusable DOM logic use:action for DOM enhancements onMount for each usage <div use:clickOutside> onMount(() => setupClickOutside(el)) Medium https://svelte.dev/docs/element-directives#use-action
36 35 Actions Return update and destroy Lifecycle methods for actions Return { update, destroy } Only initial setup return { update(params) {}, destroy() {} } return destroy only Medium
37 36 Actions Pass parameters to actions Configure action behavior use:action={params} Hardcoded action behavior <div use:tooltip={options}> <div use:tooltip> Low
38 37 Logic Use {#if} for conditionals Template conditionals {#if} {:else if} {:else} Ternary in expressions {#if cond}...{:else}...{/if} {cond ? a : b} for complex Low https://svelte.dev/docs/logic-blocks#if
39 38 Logic Use {#each} for lists List rendering {#each} with key Map in expression {#each items as item (item.id)} {items.map(i => `<div>${i}</div>`)} Medium
40 39 Logic Always use keys in {#each} Proper list reconciliation (item.id) for unique key Index as key or no key {#each items as item (item.id)} {#each items as item, i (i)} High
41 40 Logic Use {#await} for promises Handle async states {#await} for loading/error states Manual promise handling {#await promise}...{:then}...{:catch} {#if loading}...{#if error} Medium https://svelte.dev/docs/logic-blocks#await
42 41 SvelteKit Use +page.svelte for routes File-based routing +page.svelte for route components Custom routing setup routes/about/+page.svelte routes/About.svelte Medium https://kit.svelte.dev/docs/routing
43 42 SvelteKit Use +page.js for data loading Load data before render load function in +page.js onMount for data fetching export function load() {} onMount(() => fetchData()) High https://kit.svelte.dev/docs/load
44 43 SvelteKit Use +page.server.js for server-only Server-side data loading +page.server.js for sensitive data +page.js for API keys +page.server.js with DB access +page.js with DB access High
45 44 SvelteKit Use form actions Server-side form handling +page.server.js actions API routes for forms export const actions = { default } fetch('/api/submit') Medium https://kit.svelte.dev/docs/form-actions
46 45 SvelteKit Use $app/stores for app state $page $navigating $updated $page for current page data Manual URL parsing import { page } from '$app/stores' window.location.pathname Medium https://kit.svelte.dev/docs/modules#$app-stores
47 46 Performance Use {#key} for forced re-render Reset component state {#key id} for fresh instance Manual destroy/create {#key item.id}<Component/>{/key} on:change={() => component = null} Low https://svelte.dev/docs/logic-blocks#key
48 47 Performance Avoid unnecessary reactivity Not everything needs $: $: only for side effects $: for simple assignments $: if (x) console.log(x) $: y = x (when y = x works) Low
49 48 Performance Use immutable compiler option Skip equality checks immutable: true for large lists Default for all components <svelte:options immutable/> Default without immutable Low
50 49 TypeScript Use lang="ts" in script TypeScript support <script lang="ts"> JavaScript for typed projects <script lang="ts"> <script> with JSDoc Medium https://svelte.dev/docs/typescript
51 50 TypeScript Type props with interface Explicit prop types interface $$Props for types Untyped props interface $$Props { name: string } export let name Medium
52 51 TypeScript Type events with createEventDispatcher Type-safe events createEventDispatcher<Events>() Untyped dispatch createEventDispatcher<{ save: Data }>() createEventDispatcher() Medium
53 52 Accessibility Use semantic elements Proper HTML in templates button nav main appropriately div for everything <button on:click> <div on:click> High
54 53 Accessibility Add aria to dynamic content Accessible state changes aria-live for updates Silent dynamic updates <div aria-live="polite">{message}</div> <div>{message}</div> Medium

View File

@@ -1,51 +1,51 @@
No,Category,Guideline,Description,Do,Don't,Code Good,Code Bad,Severity,Docs URL No,Category,Guideline,Description,Do,Don't,Code Good,Code Bad,Severity,Docs URL
1,Views,Use struct for views,SwiftUI views are value types,struct MyView: View,class MyView: View,struct ContentView: View { var body: some View },class ContentView: View,High,https://developer.apple.com/documentation/swiftui/view 1,Views,Use struct for views,SwiftUI views are value types,struct MyView: View,class MyView: View,struct ContentView: View { var body: some View },class ContentView: View,High,https://developer.apple.com/documentation/swiftui/view
2,Views,Keep views small and focused,Single responsibility for each view,Extract subviews for complex layouts,Large monolithic views,Extract HeaderView FooterView,500+ line View struct,Medium, 2,Views,Keep views small and focused,Single responsibility for each view,Extract subviews for complex layouts,Large monolithic views,Extract HeaderView FooterView,500+ line View struct,Medium,
3,Views,Use body computed property,body returns the view hierarchy,var body: some View { },func body() -> some View,"var body: some View { Text(""Hello"") }",func body() -> Text,High, 3,Views,Use body computed property,body returns the view hierarchy,var body: some View { },func body() -> some View,"var body: some View { Text(""Hello"") }",func body() -> Text,High,
4,Views,Prefer composition over inheritance,Compose views using ViewBuilder,Combine smaller views,Inheritance hierarchies,VStack { Header() Content() },class SpecialView extends BaseView,Medium, 4,Views,Prefer composition over inheritance,Compose views using ViewBuilder,Combine smaller views,Inheritance hierarchies,VStack { Header() Content() },class SpecialView extends BaseView,Medium,
5,State,Use @State for local state,Simple value types owned by view,@State for view-local primitives,@State for shared data,@State private var count = 0,@State var sharedData: Model,High,https://developer.apple.com/documentation/swiftui/state 5,State,Use @State for local state,Simple value types owned by view,@State for view-local primitives,@State for shared data,@State private var count = 0,@State var sharedData: Model,High,https://developer.apple.com/documentation/swiftui/state
6,State,Use @Binding for two-way data,Pass mutable state to child views,@Binding for child input,@State in child for parent data,@Binding var isOn: Bool,$isOn to pass binding,Medium,https://developer.apple.com/documentation/swiftui/binding 6,State,Use @Binding for two-way data,Pass mutable state to child views,@Binding for child input,@State in child for parent data,@Binding var isOn: Bool,$isOn to pass binding,Medium,https://developer.apple.com/documentation/swiftui/binding
7,State,Use @StateObject for reference types,ObservableObject owned by view,@StateObject for view-created objects,@ObservedObject for owned objects,@StateObject private var vm = ViewModel(),@ObservedObject var vm = ViewModel(),High,https://developer.apple.com/documentation/swiftui/stateobject 7,State,Use @StateObject for reference types,ObservableObject owned by view,@StateObject for view-created objects,@ObservedObject for owned objects,@StateObject private var vm = ViewModel(),@ObservedObject var vm = ViewModel(),High,https://developer.apple.com/documentation/swiftui/stateobject
8,State,Use @ObservedObject for injected objects,Reference types passed from parent,@ObservedObject for injected dependencies,@StateObject for injected objects,@ObservedObject var vm: ViewModel,@StateObject var vm: ViewModel (injected),High,https://developer.apple.com/documentation/swiftui/observedobject 8,State,Use @ObservedObject for injected objects,Reference types passed from parent,@ObservedObject for injected dependencies,@StateObject for injected objects,@ObservedObject var vm: ViewModel,@StateObject var vm: ViewModel (injected),High,https://developer.apple.com/documentation/swiftui/observedobject
9,State,Use @EnvironmentObject for shared state,App-wide state injection,@EnvironmentObject for global state,Prop drilling through views,@EnvironmentObject var settings: Settings,Pass settings through 5 views,Medium,https://developer.apple.com/documentation/swiftui/environmentobject 9,State,Use @EnvironmentObject for shared state,App-wide state injection,@EnvironmentObject for global state,Prop drilling through views,@EnvironmentObject var settings: Settings,Pass settings through 5 views,Medium,https://developer.apple.com/documentation/swiftui/environmentobject
10,State,Use @Published in ObservableObject,Automatically publish property changes,@Published for observed properties,Manual objectWillChange calls,@Published var items: [Item] = [],var items: [Item] { didSet { objectWillChange.send() } },Medium, 10,State,Use @Published in ObservableObject,Automatically publish property changes,@Published for observed properties,Manual objectWillChange calls,@Published var items: [Item] = [],var items: [Item] { didSet { objectWillChange.send() } },Medium,
11,Observable,Use @Observable macro (iOS 17+),Modern observation without Combine,@Observable class for view models,ObservableObject for new projects,@Observable class ViewModel { },class ViewModel: ObservableObject,Medium,https://developer.apple.com/documentation/observation 11,Observable,Use @Observable macro (iOS 17+),Modern observation without Combine,@Observable class for view models,ObservableObject for new projects,@Observable class ViewModel { },class ViewModel: ObservableObject,Medium,https://developer.apple.com/documentation/observation
12,Observable,Use @Bindable for @Observable,Create bindings from @Observable,@Bindable var vm for bindings,@Binding with @Observable,@Bindable var viewModel,$viewModel.name with @Observable,Medium, 12,Observable,Use @Bindable for @Observable,Create bindings from @Observable,@Bindable var vm for bindings,@Binding with @Observable,@Bindable var viewModel,$viewModel.name with @Observable,Medium,
13,Layout,Use VStack HStack ZStack,Standard stack-based layouts,Stacks for linear arrangements,GeometryReader for simple layouts,VStack { Text() Image() },GeometryReader for vertical list,Medium,https://developer.apple.com/documentation/swiftui/vstack 13,Layout,Use VStack HStack ZStack,Standard stack-based layouts,Stacks for linear arrangements,GeometryReader for simple layouts,VStack { Text() Image() },GeometryReader for vertical list,Medium,https://developer.apple.com/documentation/swiftui/vstack
14,Layout,Use LazyVStack LazyHStack for lists,Lazy loading for performance,Lazy stacks for long lists,Regular stacks for 100+ items,LazyVStack { ForEach(items) },VStack { ForEach(largeArray) },High,https://developer.apple.com/documentation/swiftui/lazyvstack 14,Layout,Use LazyVStack LazyHStack for lists,Lazy loading for performance,Lazy stacks for long lists,Regular stacks for 100+ items,LazyVStack { ForEach(items) },VStack { ForEach(largeArray) },High,https://developer.apple.com/documentation/swiftui/lazyvstack
15,Layout,Use GeometryReader sparingly,Only when needed for sizing,GeometryReader for responsive layouts,GeometryReader everywhere,GeometryReader for aspect ratio,GeometryReader wrapping everything,Medium, 15,Layout,Use GeometryReader sparingly,Only when needed for sizing,GeometryReader for responsive layouts,GeometryReader everywhere,GeometryReader for aspect ratio,GeometryReader wrapping everything,Medium,
16,Layout,Use spacing and padding consistently,Consistent spacing throughout app,Design system spacing values,Magic numbers for spacing,.padding(16) or .padding(),".padding(13), .padding(17)",Low, 16,Layout,Use spacing and padding consistently,Consistent spacing throughout app,Design system spacing values,Magic numbers for spacing,.padding(16) or .padding(),".padding(13), .padding(17)",Low,
17,Layout,Use frame modifiers correctly,Set explicit sizes when needed,.frame(maxWidth: .infinity),Fixed sizes for responsive content,.frame(maxWidth: .infinity),.frame(width: 375),Medium, 17,Layout,Use frame modifiers correctly,Set explicit sizes when needed,.frame(maxWidth: .infinity),Fixed sizes for responsive content,.frame(maxWidth: .infinity),.frame(width: 375),Medium,
18,Modifiers,Order modifiers correctly,Modifier order affects rendering,Background before padding for full coverage,Wrong modifier order,.padding().background(Color.red),.background(Color.red).padding(),High, 18,Modifiers,Order modifiers correctly,Modifier order affects rendering,Background before padding for full coverage,Wrong modifier order,.padding().background(Color.red),.background(Color.red).padding(),High,
19,Modifiers,Create custom ViewModifiers,Reusable modifier combinations,ViewModifier for repeated styling,Duplicate modifier chains,struct CardStyle: ViewModifier,.shadow().cornerRadius() everywhere,Medium,https://developer.apple.com/documentation/swiftui/viewmodifier 19,Modifiers,Create custom ViewModifiers,Reusable modifier combinations,ViewModifier for repeated styling,Duplicate modifier chains,struct CardStyle: ViewModifier,.shadow().cornerRadius() everywhere,Medium,https://developer.apple.com/documentation/swiftui/viewmodifier
20,Modifiers,Use conditional modifiers carefully,Avoid changing view identity,if-else with same view type,Conditional that changes view identity,Text(title).foregroundColor(isActive ? .blue : .gray),if isActive { Text().bold() } else { Text() },Medium, 20,Modifiers,Use conditional modifiers carefully,Avoid changing view identity,if-else with same view type,Conditional that changes view identity,Text(title).foregroundColor(isActive ? .blue : .gray),if isActive { Text().bold() } else { Text() },Medium,
21,Navigation,Use NavigationStack (iOS 16+),Modern navigation with type-safe paths,NavigationStack with navigationDestination,NavigationView for new projects,NavigationStack { },NavigationView { } (deprecated),Medium,https://developer.apple.com/documentation/swiftui/navigationstack 21,Navigation,Use NavigationStack (iOS 16+),Modern navigation with type-safe paths,NavigationStack with navigationDestination,NavigationView for new projects,NavigationStack { },NavigationView { } (deprecated),Medium,https://developer.apple.com/documentation/swiftui/navigationstack
22,Navigation,Use navigationDestination,Type-safe navigation destinations,.navigationDestination(for:),NavigationLink(destination:),.navigationDestination(for: Item.self),NavigationLink(destination: DetailView()),Medium, 22,Navigation,Use navigationDestination,Type-safe navigation destinations,.navigationDestination(for:),NavigationLink(destination:),.navigationDestination(for: Item.self),NavigationLink(destination: DetailView()),Medium,
23,Navigation,Use @Environment for dismiss,Programmatic navigation dismissal,@Environment(\.dismiss) var dismiss,presentationMode (deprecated),@Environment(\.dismiss) var dismiss,@Environment(\.presentationMode),Low, 23,Navigation,Use @Environment for dismiss,Programmatic navigation dismissal,@Environment(\.dismiss) var dismiss,presentationMode (deprecated),@Environment(\.dismiss) var dismiss,@Environment(\.presentationMode),Low,
24,Lists,Use List for scrollable content,Built-in scrolling and styling,List for standard scrollable content,ScrollView + VStack for simple lists,List { ForEach(items) { } },ScrollView { VStack { ForEach } },Low,https://developer.apple.com/documentation/swiftui/list 24,Lists,Use List for scrollable content,Built-in scrolling and styling,List for standard scrollable content,ScrollView + VStack for simple lists,List { ForEach(items) { } },ScrollView { VStack { ForEach } },Low,https://developer.apple.com/documentation/swiftui/list
25,Lists,Provide stable identifiers,Use Identifiable or explicit id,Identifiable protocol or id parameter,Index as identifier,ForEach(items) where Item: Identifiable,"ForEach(items.indices, id: \.self)",High, 25,Lists,Provide stable identifiers,Use Identifiable or explicit id,Identifiable protocol or id parameter,Index as identifier,ForEach(items) where Item: Identifiable,"ForEach(items.indices, id: \.self)",High,
26,Lists,Use onDelete and onMove,Standard list editing,onDelete for swipe to delete,Custom delete implementation,.onDelete(perform: delete),.onTapGesture for delete,Low, 26,Lists,Use onDelete and onMove,Standard list editing,onDelete for swipe to delete,Custom delete implementation,.onDelete(perform: delete),.onTapGesture for delete,Low,
27,Forms,Use Form for settings,Grouped input controls,Form for settings screens,Manual grouping for forms,Form { Section { Toggle() } },VStack { Toggle() },Low,https://developer.apple.com/documentation/swiftui/form 27,Forms,Use Form for settings,Grouped input controls,Form for settings screens,Manual grouping for forms,Form { Section { Toggle() } },VStack { Toggle() },Low,https://developer.apple.com/documentation/swiftui/form
28,Forms,Use @FocusState for keyboard,Manage keyboard focus,@FocusState for text field focus,Manual first responder handling,@FocusState private var isFocused: Bool,UIKit first responder,Medium,https://developer.apple.com/documentation/swiftui/focusstate 28,Forms,Use @FocusState for keyboard,Manage keyboard focus,@FocusState for text field focus,Manual first responder handling,@FocusState private var isFocused: Bool,UIKit first responder,Medium,https://developer.apple.com/documentation/swiftui/focusstate
29,Forms,Validate input properly,Show validation feedback,Real-time validation feedback,Submit without validation,TextField with validation state,TextField without error handling,Medium, 29,Forms,Validate input properly,Show validation feedback,Real-time validation feedback,Submit without validation,TextField with validation state,TextField without error handling,Medium,
30,Async,Use .task for async work,Automatic cancellation on view disappear,.task for view lifecycle async,onAppear with Task,.task { await loadData() },onAppear { Task { await loadData() } },Medium,https://developer.apple.com/documentation/swiftui/view/task(priority:_:) 30,Async,Use .task for async work,Automatic cancellation on view disappear,.task for view lifecycle async,onAppear with Task,.task { await loadData() },onAppear { Task { await loadData() } },Medium,https://developer.apple.com/documentation/swiftui/view/task(priority:_:)
31,Async,Handle loading states,Show progress during async operations,ProgressView during loading,Empty view during load,if isLoading { ProgressView() },No loading indicator,Medium, 31,Async,Handle loading states,Show progress during async operations,ProgressView during loading,Empty view during load,if isLoading { ProgressView() },No loading indicator,Medium,
32,Async,Use @MainActor for UI updates,Ensure UI updates on main thread,@MainActor on view models,Manual DispatchQueue.main,@MainActor class ViewModel,DispatchQueue.main.async,Medium, 32,Async,Use @MainActor for UI updates,Ensure UI updates on main thread,@MainActor on view models,Manual DispatchQueue.main,@MainActor class ViewModel,DispatchQueue.main.async,Medium,
33,Animation,Use withAnimation,Animate state changes,withAnimation for state transitions,No animation for state changes,withAnimation { isExpanded.toggle() },isExpanded.toggle(),Low,https://developer.apple.com/documentation/swiftui/withanimation(_:_:) 33,Animation,Use withAnimation,Animate state changes,withAnimation for state transitions,No animation for state changes,withAnimation { isExpanded.toggle() },isExpanded.toggle(),Low,https://developer.apple.com/documentation/swiftui/withanimation(_:_:)
34,Animation,Use .animation modifier,Apply animations to views,.animation(.spring()) on view,Manual animation timing,.animation(.easeInOut),CABasicAnimation equivalent,Low, 34,Animation,Use .animation modifier,Apply animations to views,.animation(.spring()) on view,Manual animation timing,.animation(.easeInOut),CABasicAnimation equivalent,Low,
35,Animation,Respect reduced motion,Check accessibility settings,Check accessibilityReduceMotion,Ignore motion preferences,@Environment(\.accessibilityReduceMotion),Always animate regardless,High, 35,Animation,Respect reduced motion,Check accessibility settings,Check accessibilityReduceMotion,Ignore motion preferences,@Environment(\.accessibilityReduceMotion),Always animate regardless,High,
36,Preview,Use #Preview macro (Xcode 15+),Modern preview syntax,#Preview for view previews,PreviewProvider protocol,#Preview { ContentView() },struct ContentView_Previews: PreviewProvider,Low, 36,Preview,Use #Preview macro (Xcode 15+),Modern preview syntax,#Preview for view previews,PreviewProvider protocol,#Preview { ContentView() },struct ContentView_Previews: PreviewProvider,Low,
37,Preview,Create multiple previews,Test different states and devices,Multiple previews for states,Single preview only,"#Preview(""Light"") { } #Preview(""Dark"") { }",Single preview configuration,Low, 37,Preview,Create multiple previews,Test different states and devices,Multiple previews for states,Single preview only,"#Preview(""Light"") { } #Preview(""Dark"") { }",Single preview configuration,Low,
38,Preview,Use preview data,Dedicated preview mock data,Static preview data,Production data in previews,Item.preview for preview,Fetch real data in preview,Low, 38,Preview,Use preview data,Dedicated preview mock data,Static preview data,Production data in previews,Item.preview for preview,Fetch real data in preview,Low,
39,Performance,Avoid expensive body computations,Body should be fast to compute,Precompute in view model,Heavy computation in body,vm.computedValue in body,Complex calculation in body,High, 39,Performance,Avoid expensive body computations,Body should be fast to compute,Precompute in view model,Heavy computation in body,vm.computedValue in body,Complex calculation in body,High,
40,Performance,Use Equatable views,Skip unnecessary view updates,Equatable for complex views,Default equality for all views,struct MyView: View Equatable,No Equatable conformance,Medium, 40,Performance,Use Equatable views,Skip unnecessary view updates,Equatable for complex views,Default equality for all views,struct MyView: View Equatable,No Equatable conformance,Medium,
41,Performance,Profile with Instruments,Measure before optimizing,Use SwiftUI Instruments,Guess at performance issues,Profile with Instruments,Optimize without measuring,Medium, 41,Performance,Profile with Instruments,Measure before optimizing,Use SwiftUI Instruments,Guess at performance issues,Profile with Instruments,Optimize without measuring,Medium,
42,Accessibility,Add accessibility labels,Describe UI elements,.accessibilityLabel for context,Missing labels,".accessibilityLabel(""Close button"")",Button without label,High,https://developer.apple.com/documentation/swiftui/view/accessibilitylabel(_:)-1d7jv 42,Accessibility,Add accessibility labels,Describe UI elements,.accessibilityLabel for context,Missing labels,".accessibilityLabel(""Close button"")",Button without label,High,https://developer.apple.com/documentation/swiftui/view/accessibilitylabel(_:)-1d7jv
43,Accessibility,Support Dynamic Type,Respect text size preferences,Scalable fonts and layouts,Fixed font sizes,.font(.body) with Dynamic Type,.font(.system(size: 16)),High, 43,Accessibility,Support Dynamic Type,Respect text size preferences,Scalable fonts and layouts,Fixed font sizes,.font(.body) with Dynamic Type,.font(.system(size: 16)),High,
44,Accessibility,Use semantic views,Proper accessibility traits,Correct accessibilityTraits,Wrong semantic meaning,Button for actions Image for display,Image that acts like button,Medium, 44,Accessibility,Use semantic views,Proper accessibility traits,Correct accessibilityTraits,Wrong semantic meaning,Button for actions Image for display,Image that acts like button,Medium,
45,Testing,Use ViewInspector for testing,Third-party view testing,ViewInspector for unit tests,UI tests only,ViewInspector assertions,Only XCUITest,Medium, 45,Testing,Use ViewInspector for testing,Third-party view testing,ViewInspector for unit tests,UI tests only,ViewInspector assertions,Only XCUITest,Medium,
46,Testing,Test view models,Unit test business logic,XCTest for view model,Skip view model testing,Test ViewModel methods,No unit tests,Medium, 46,Testing,Test view models,Unit test business logic,XCTest for view model,Skip view model testing,Test ViewModel methods,No unit tests,Medium,
47,Testing,Use preview as visual test,Previews catch visual regressions,Multiple preview configurations,No visual verification,Preview different states,Single preview only,Low, 47,Testing,Use preview as visual test,Previews catch visual regressions,Multiple preview configurations,No visual verification,Preview different states,Single preview only,Low,
48,Architecture,Use MVVM pattern,Separate view and logic,ViewModel for business logic,Logic in View,ObservableObject ViewModel,@State for complex logic,Medium, 48,Architecture,Use MVVM pattern,Separate view and logic,ViewModel for business logic,Logic in View,ObservableObject ViewModel,@State for complex logic,Medium,
49,Architecture,Keep views dumb,Views display view model state,View reads from ViewModel,Business logic in View,view.items from vm.items,Complex filtering in View,Medium, 49,Architecture,Keep views dumb,Views display view model state,View reads from ViewModel,Business logic in View,view.items from vm.items,Complex filtering in View,Medium,
50,Architecture,Use dependency injection,Inject dependencies for testing,Initialize with dependencies,Hard-coded dependencies,init(service: ServiceProtocol),let service = RealService(),Medium, 50,Architecture,Use dependency injection,Inject dependencies for testing,Initialize with dependencies,Hard-coded dependencies,init(service: ServiceProtocol),let service = RealService(),Medium,
1 No Category Guideline Description Do Don't Code Good Code Bad Severity Docs URL
2 1 Views Use struct for views SwiftUI views are value types struct MyView: View class MyView: View struct ContentView: View { var body: some View } class ContentView: View High https://developer.apple.com/documentation/swiftui/view
3 2 Views Keep views small and focused Single responsibility for each view Extract subviews for complex layouts Large monolithic views Extract HeaderView FooterView 500+ line View struct Medium
4 3 Views Use body computed property body returns the view hierarchy var body: some View { } func body() -> some View var body: some View { Text("Hello") } func body() -> Text High
5 4 Views Prefer composition over inheritance Compose views using ViewBuilder Combine smaller views Inheritance hierarchies VStack { Header() Content() } class SpecialView extends BaseView Medium
6 5 State Use @State for local state Simple value types owned by view @State for view-local primitives @State for shared data @State private var count = 0 @State var sharedData: Model High https://developer.apple.com/documentation/swiftui/state
7 6 State Use @Binding for two-way data Pass mutable state to child views @Binding for child input @State in child for parent data @Binding var isOn: Bool $isOn to pass binding Medium https://developer.apple.com/documentation/swiftui/binding
8 7 State Use @StateObject for reference types ObservableObject owned by view @StateObject for view-created objects @ObservedObject for owned objects @StateObject private var vm = ViewModel() @ObservedObject var vm = ViewModel() High https://developer.apple.com/documentation/swiftui/stateobject
9 8 State Use @ObservedObject for injected objects Reference types passed from parent @ObservedObject for injected dependencies @StateObject for injected objects @ObservedObject var vm: ViewModel @StateObject var vm: ViewModel (injected) High https://developer.apple.com/documentation/swiftui/observedobject
10 9 State Use @EnvironmentObject for shared state App-wide state injection @EnvironmentObject for global state Prop drilling through views @EnvironmentObject var settings: Settings Pass settings through 5 views Medium https://developer.apple.com/documentation/swiftui/environmentobject
11 10 State Use @Published in ObservableObject Automatically publish property changes @Published for observed properties Manual objectWillChange calls @Published var items: [Item] = [] var items: [Item] { didSet { objectWillChange.send() } } Medium
12 11 Observable Use @Observable macro (iOS 17+) Modern observation without Combine @Observable class for view models ObservableObject for new projects @Observable class ViewModel { } class ViewModel: ObservableObject Medium https://developer.apple.com/documentation/observation
13 12 Observable Use @Bindable for @Observable Create bindings from @Observable @Bindable var vm for bindings @Binding with @Observable @Bindable var viewModel $viewModel.name with @Observable Medium
14 13 Layout Use VStack HStack ZStack Standard stack-based layouts Stacks for linear arrangements GeometryReader for simple layouts VStack { Text() Image() } GeometryReader for vertical list Medium https://developer.apple.com/documentation/swiftui/vstack
15 14 Layout Use LazyVStack LazyHStack for lists Lazy loading for performance Lazy stacks for long lists Regular stacks for 100+ items LazyVStack { ForEach(items) } VStack { ForEach(largeArray) } High https://developer.apple.com/documentation/swiftui/lazyvstack
16 15 Layout Use GeometryReader sparingly Only when needed for sizing GeometryReader for responsive layouts GeometryReader everywhere GeometryReader for aspect ratio GeometryReader wrapping everything Medium
17 16 Layout Use spacing and padding consistently Consistent spacing throughout app Design system spacing values Magic numbers for spacing .padding(16) or .padding() .padding(13), .padding(17) Low
18 17 Layout Use frame modifiers correctly Set explicit sizes when needed .frame(maxWidth: .infinity) Fixed sizes for responsive content .frame(maxWidth: .infinity) .frame(width: 375) Medium
19 18 Modifiers Order modifiers correctly Modifier order affects rendering Background before padding for full coverage Wrong modifier order .padding().background(Color.red) .background(Color.red).padding() High
20 19 Modifiers Create custom ViewModifiers Reusable modifier combinations ViewModifier for repeated styling Duplicate modifier chains struct CardStyle: ViewModifier .shadow().cornerRadius() everywhere Medium https://developer.apple.com/documentation/swiftui/viewmodifier
21 20 Modifiers Use conditional modifiers carefully Avoid changing view identity if-else with same view type Conditional that changes view identity Text(title).foregroundColor(isActive ? .blue : .gray) if isActive { Text().bold() } else { Text() } Medium
22 21 Navigation Use NavigationStack (iOS 16+) Modern navigation with type-safe paths NavigationStack with navigationDestination NavigationView for new projects NavigationStack { } NavigationView { } (deprecated) Medium https://developer.apple.com/documentation/swiftui/navigationstack
23 22 Navigation Use navigationDestination Type-safe navigation destinations .navigationDestination(for:) NavigationLink(destination:) .navigationDestination(for: Item.self) NavigationLink(destination: DetailView()) Medium
24 23 Navigation Use @Environment for dismiss Programmatic navigation dismissal @Environment(\.dismiss) var dismiss presentationMode (deprecated) @Environment(\.dismiss) var dismiss @Environment(\.presentationMode) Low
25 24 Lists Use List for scrollable content Built-in scrolling and styling List for standard scrollable content ScrollView + VStack for simple lists List { ForEach(items) { } } ScrollView { VStack { ForEach } } Low https://developer.apple.com/documentation/swiftui/list
26 25 Lists Provide stable identifiers Use Identifiable or explicit id Identifiable protocol or id parameter Index as identifier ForEach(items) where Item: Identifiable ForEach(items.indices, id: \.self) High
27 26 Lists Use onDelete and onMove Standard list editing onDelete for swipe to delete Custom delete implementation .onDelete(perform: delete) .onTapGesture for delete Low
28 27 Forms Use Form for settings Grouped input controls Form for settings screens Manual grouping for forms Form { Section { Toggle() } } VStack { Toggle() } Low https://developer.apple.com/documentation/swiftui/form
29 28 Forms Use @FocusState for keyboard Manage keyboard focus @FocusState for text field focus Manual first responder handling @FocusState private var isFocused: Bool UIKit first responder Medium https://developer.apple.com/documentation/swiftui/focusstate
30 29 Forms Validate input properly Show validation feedback Real-time validation feedback Submit without validation TextField with validation state TextField without error handling Medium
31 30 Async Use .task for async work Automatic cancellation on view disappear .task for view lifecycle async onAppear with Task .task { await loadData() } onAppear { Task { await loadData() } } Medium https://developer.apple.com/documentation/swiftui/view/task(priority:_:)
32 31 Async Handle loading states Show progress during async operations ProgressView during loading Empty view during load if isLoading { ProgressView() } No loading indicator Medium
33 32 Async Use @MainActor for UI updates Ensure UI updates on main thread @MainActor on view models Manual DispatchQueue.main @MainActor class ViewModel DispatchQueue.main.async Medium
34 33 Animation Use withAnimation Animate state changes withAnimation for state transitions No animation for state changes withAnimation { isExpanded.toggle() } isExpanded.toggle() Low https://developer.apple.com/documentation/swiftui/withanimation(_:_:)
35 34 Animation Use .animation modifier Apply animations to views .animation(.spring()) on view Manual animation timing .animation(.easeInOut) CABasicAnimation equivalent Low
36 35 Animation Respect reduced motion Check accessibility settings Check accessibilityReduceMotion Ignore motion preferences @Environment(\.accessibilityReduceMotion) Always animate regardless High
37 36 Preview Use #Preview macro (Xcode 15+) Modern preview syntax #Preview for view previews PreviewProvider protocol #Preview { ContentView() } struct ContentView_Previews: PreviewProvider Low
38 37 Preview Create multiple previews Test different states and devices Multiple previews for states Single preview only #Preview("Light") { } #Preview("Dark") { } Single preview configuration Low
39 38 Preview Use preview data Dedicated preview mock data Static preview data Production data in previews Item.preview for preview Fetch real data in preview Low
40 39 Performance Avoid expensive body computations Body should be fast to compute Precompute in view model Heavy computation in body vm.computedValue in body Complex calculation in body High
41 40 Performance Use Equatable views Skip unnecessary view updates Equatable for complex views Default equality for all views struct MyView: View Equatable No Equatable conformance Medium
42 41 Performance Profile with Instruments Measure before optimizing Use SwiftUI Instruments Guess at performance issues Profile with Instruments Optimize without measuring Medium
43 42 Accessibility Add accessibility labels Describe UI elements .accessibilityLabel for context Missing labels .accessibilityLabel("Close button") Button without label High https://developer.apple.com/documentation/swiftui/view/accessibilitylabel(_:)-1d7jv
44 43 Accessibility Support Dynamic Type Respect text size preferences Scalable fonts and layouts Fixed font sizes .font(.body) with Dynamic Type .font(.system(size: 16)) High
45 44 Accessibility Use semantic views Proper accessibility traits Correct accessibilityTraits Wrong semantic meaning Button for actions Image for display Image that acts like button Medium
46 45 Testing Use ViewInspector for testing Third-party view testing ViewInspector for unit tests UI tests only ViewInspector assertions Only XCUITest Medium
47 46 Testing Test view models Unit test business logic XCTest for view model Skip view model testing Test ViewModel methods No unit tests Medium
48 47 Testing Use preview as visual test Previews catch visual regressions Multiple preview configurations No visual verification Preview different states Single preview only Low
49 48 Architecture Use MVVM pattern Separate view and logic ViewModel for business logic Logic in View ObservableObject ViewModel @State for complex logic Medium
50 49 Architecture Keep views dumb Views display view model state View reads from ViewModel Business logic in View view.items from vm.items Complex filtering in View Medium
51 50 Architecture Use dependency injection Inject dependencies for testing Initialize with dependencies Hard-coded dependencies init(service: ServiceProtocol) let service = RealService() Medium

View File

@@ -1,50 +1,50 @@
No,Category,Guideline,Description,Do,Don't,Code Good,Code Bad,Severity,Docs URL No,Category,Guideline,Description,Do,Don't,Code Good,Code Bad,Severity,Docs URL
1,Composition,Use Composition API for new projects,Composition API offers better TypeScript support and logic reuse,<script setup> for components,Options API for new projects,<script setup>,export default { data() },Medium,https://vuejs.org/guide/extras/composition-api-faq.html 1,Composition,Use Composition API for new projects,Composition API offers better TypeScript support and logic reuse,<script setup> for components,Options API for new projects,<script setup>,export default { data() },Medium,https://vuejs.org/guide/extras/composition-api-faq.html
2,Composition,Use script setup syntax,Cleaner syntax with automatic exports,<script setup> with defineProps,setup() function manually,<script setup>,<script> setup() { return {} },Low,https://vuejs.org/api/sfc-script-setup.html 2,Composition,Use script setup syntax,Cleaner syntax with automatic exports,<script setup> with defineProps,setup() function manually,<script setup>,<script> setup() { return {} },Low,https://vuejs.org/api/sfc-script-setup.html
3,Reactivity,Use ref for primitives,ref() for primitive values that need reactivity,ref() for strings numbers booleans,reactive() for primitives,const count = ref(0),const count = reactive(0),Medium,https://vuejs.org/guide/essentials/reactivity-fundamentals.html 3,Reactivity,Use ref for primitives,ref() for primitive values that need reactivity,ref() for strings numbers booleans,reactive() for primitives,const count = ref(0),const count = reactive(0),Medium,https://vuejs.org/guide/essentials/reactivity-fundamentals.html
4,Reactivity,Use reactive for objects,reactive() for complex objects and arrays,reactive() for objects with multiple properties,ref() for complex objects,const state = reactive({ user: null }),const state = ref({ user: null }),Medium, 4,Reactivity,Use reactive for objects,reactive() for complex objects and arrays,reactive() for objects with multiple properties,ref() for complex objects,const state = reactive({ user: null }),const state = ref({ user: null }),Medium,
5,Reactivity,Access ref values with .value,Remember .value in script unwrap in template,Use .value in script,Forget .value in script,count.value++,count++ (in script),High, 5,Reactivity,Access ref values with .value,Remember .value in script unwrap in template,Use .value in script,Forget .value in script,count.value++,count++ (in script),High,
6,Reactivity,Use computed for derived state,Computed properties cache and update automatically,computed() for derived values,Methods for derived values,const doubled = computed(() => count.value * 2),const doubled = () => count.value * 2,Medium,https://vuejs.org/guide/essentials/computed.html 6,Reactivity,Use computed for derived state,Computed properties cache and update automatically,computed() for derived values,Methods for derived values,const doubled = computed(() => count.value * 2),const doubled = () => count.value * 2,Medium,https://vuejs.org/guide/essentials/computed.html
7,Reactivity,Use shallowRef for large objects,Avoid deep reactivity for performance,shallowRef for large data structures,ref for large nested objects,const bigData = shallowRef(largeObject),const bigData = ref(largeObject),Medium,https://vuejs.org/api/reactivity-advanced.html#shallowref 7,Reactivity,Use shallowRef for large objects,Avoid deep reactivity for performance,shallowRef for large data structures,ref for large nested objects,const bigData = shallowRef(largeObject),const bigData = ref(largeObject),Medium,https://vuejs.org/api/reactivity-advanced.html#shallowref
8,Watchers,Use watchEffect for simple cases,Auto-tracks dependencies,watchEffect for simple reactive effects,watch with explicit deps when not needed,watchEffect(() => console.log(count.value)),"watch(count, (val) => console.log(val))",Low,https://vuejs.org/guide/essentials/watchers.html 8,Watchers,Use watchEffect for simple cases,Auto-tracks dependencies,watchEffect for simple reactive effects,watch with explicit deps when not needed,watchEffect(() => console.log(count.value)),"watch(count, (val) => console.log(val))",Low,https://vuejs.org/guide/essentials/watchers.html
9,Watchers,Use watch for specific sources,Explicit control over what to watch,watch with specific refs,watchEffect for complex conditional logic,"watch(userId, fetchUser)",watchEffect with conditionals,Medium, 9,Watchers,Use watch for specific sources,Explicit control over what to watch,watch with specific refs,watchEffect for complex conditional logic,"watch(userId, fetchUser)",watchEffect with conditionals,Medium,
10,Watchers,Clean up side effects,Return cleanup function in watchers,Return cleanup in watchEffect,Leave subscriptions open,watchEffect((onCleanup) => { onCleanup(unsub) }),watchEffect without cleanup,High, 10,Watchers,Clean up side effects,Return cleanup function in watchers,Return cleanup in watchEffect,Leave subscriptions open,watchEffect((onCleanup) => { onCleanup(unsub) }),watchEffect without cleanup,High,
11,Props,Define props with defineProps,Type-safe prop definitions,defineProps with TypeScript,Props without types,defineProps<{ msg: string }>(),defineProps(['msg']),Medium,https://vuejs.org/guide/typescript/composition-api.html#typing-component-props 11,Props,Define props with defineProps,Type-safe prop definitions,defineProps with TypeScript,Props without types,defineProps<{ msg: string }>(),defineProps(['msg']),Medium,https://vuejs.org/guide/typescript/composition-api.html#typing-component-props
12,Props,Use withDefaults for default values,Provide defaults for optional props,withDefaults with defineProps,Defaults in destructuring,"withDefaults(defineProps<Props>(), { count: 0 })",const { count = 0 } = defineProps(),Medium, 12,Props,Use withDefaults for default values,Provide defaults for optional props,withDefaults with defineProps,Defaults in destructuring,"withDefaults(defineProps<Props>(), { count: 0 })",const { count = 0 } = defineProps(),Medium,
13,Props,Avoid mutating props,Props should be read-only,Emit events to parent for changes,Direct prop mutation,"emit('update:modelValue', newVal)",props.modelValue = newVal,High, 13,Props,Avoid mutating props,Props should be read-only,Emit events to parent for changes,Direct prop mutation,"emit('update:modelValue', newVal)",props.modelValue = newVal,High,
14,Emits,Define emits with defineEmits,Type-safe event emissions,defineEmits with types,Emit without definition,defineEmits<{ change: [id: number] }>(),"emit('change', id) without define",Medium,https://vuejs.org/guide/typescript/composition-api.html#typing-component-emits 14,Emits,Define emits with defineEmits,Type-safe event emissions,defineEmits with types,Emit without definition,defineEmits<{ change: [id: number] }>(),"emit('change', id) without define",Medium,https://vuejs.org/guide/typescript/composition-api.html#typing-component-emits
15,Emits,Use v-model for two-way binding,Simplified parent-child data flow,v-model with modelValue prop,:value + @input manually,"<Child v-model=""value""/>","<Child :value=""value"" @input=""value = $event""/>",Low,https://vuejs.org/guide/components/v-model.html 15,Emits,Use v-model for two-way binding,Simplified parent-child data flow,v-model with modelValue prop,:value + @input manually,"<Child v-model=""value""/>","<Child :value=""value"" @input=""value = $event""/>",Low,https://vuejs.org/guide/components/v-model.html
16,Lifecycle,Use onMounted for DOM access,DOM is ready in onMounted,onMounted for DOM operations,Access DOM in setup directly,onMounted(() => el.value.focus()),el.value.focus() in setup,High,https://vuejs.org/api/composition-api-lifecycle.html 16,Lifecycle,Use onMounted for DOM access,DOM is ready in onMounted,onMounted for DOM operations,Access DOM in setup directly,onMounted(() => el.value.focus()),el.value.focus() in setup,High,https://vuejs.org/api/composition-api-lifecycle.html
17,Lifecycle,Clean up in onUnmounted,Remove listeners and subscriptions,onUnmounted for cleanup,Leave listeners attached,onUnmounted(() => window.removeEventListener()),No cleanup on unmount,High, 17,Lifecycle,Clean up in onUnmounted,Remove listeners and subscriptions,onUnmounted for cleanup,Leave listeners attached,onUnmounted(() => window.removeEventListener()),No cleanup on unmount,High,
18,Lifecycle,Avoid onBeforeMount for data,Use onMounted or setup for data fetching,Fetch in onMounted or setup,Fetch in onBeforeMount,onMounted(async () => await fetchData()),onBeforeMount(async () => await fetchData()),Low, 18,Lifecycle,Avoid onBeforeMount for data,Use onMounted or setup for data fetching,Fetch in onMounted or setup,Fetch in onBeforeMount,onMounted(async () => await fetchData()),onBeforeMount(async () => await fetchData()),Low,
19,Components,Use single-file components,Keep template script style together,.vue files for components,Separate template/script files,Component.vue with all parts,Component.js + Component.html,Low, 19,Components,Use single-file components,Keep template script style together,.vue files for components,Separate template/script files,Component.vue with all parts,Component.js + Component.html,Low,
20,Components,Use PascalCase for components,Consistent component naming,PascalCase in imports and templates,kebab-case in script,<MyComponent/>,<my-component/>,Low,https://vuejs.org/style-guide/rules-strongly-recommended.html 20,Components,Use PascalCase for components,Consistent component naming,PascalCase in imports and templates,kebab-case in script,<MyComponent/>,<my-component/>,Low,https://vuejs.org/style-guide/rules-strongly-recommended.html
21,Components,Prefer composition over mixins,Composables replace mixins,Composables for shared logic,Mixins for code reuse,const { data } = useApi(),mixins: [apiMixin],Medium, 21,Components,Prefer composition over mixins,Composables replace mixins,Composables for shared logic,Mixins for code reuse,const { data } = useApi(),mixins: [apiMixin],Medium,
22,Composables,Name composables with use prefix,Convention for composable functions,useFetch useAuth useForm,getData or fetchApi,export function useFetch(),export function fetchData(),Medium,https://vuejs.org/guide/reusability/composables.html 22,Composables,Name composables with use prefix,Convention for composable functions,useFetch useAuth useForm,getData or fetchApi,export function useFetch(),export function fetchData(),Medium,https://vuejs.org/guide/reusability/composables.html
23,Composables,Return refs from composables,Maintain reactivity when destructuring,Return ref values,Return reactive objects that lose reactivity,return { data: ref(null) },return reactive({ data: null }),Medium, 23,Composables,Return refs from composables,Maintain reactivity when destructuring,Return ref values,Return reactive objects that lose reactivity,return { data: ref(null) },return reactive({ data: null }),Medium,
24,Composables,Accept ref or value params,Use toValue for flexible inputs,toValue() or unref() for params,Only accept ref or only value,const val = toValue(maybeRef),const val = maybeRef.value,Low,https://vuejs.org/api/reactivity-utilities.html#tovalue 24,Composables,Accept ref or value params,Use toValue for flexible inputs,toValue() or unref() for params,Only accept ref or only value,const val = toValue(maybeRef),const val = maybeRef.value,Low,https://vuejs.org/api/reactivity-utilities.html#tovalue
25,Templates,Use v-bind shorthand,Cleaner template syntax,:prop instead of v-bind:prop,Full v-bind syntax,"<div :class=""cls"">","<div v-bind:class=""cls"">",Low, 25,Templates,Use v-bind shorthand,Cleaner template syntax,:prop instead of v-bind:prop,Full v-bind syntax,"<div :class=""cls"">","<div v-bind:class=""cls"">",Low,
26,Templates,Use v-on shorthand,Cleaner event binding,@event instead of v-on:event,Full v-on syntax,"<button @click=""handler"">","<button v-on:click=""handler"">",Low, 26,Templates,Use v-on shorthand,Cleaner event binding,@event instead of v-on:event,Full v-on syntax,"<button @click=""handler"">","<button v-on:click=""handler"">",Low,
27,Templates,Avoid v-if with v-for,v-if has higher priority causes issues,Wrap in template or computed filter,v-if on same element as v-for,<template v-for><div v-if>,<div v-for v-if>,High,https://vuejs.org/style-guide/rules-essential.html#avoid-v-if-with-v-for 27,Templates,Avoid v-if with v-for,v-if has higher priority causes issues,Wrap in template or computed filter,v-if on same element as v-for,<template v-for><div v-if>,<div v-for v-if>,High,https://vuejs.org/style-guide/rules-essential.html#avoid-v-if-with-v-for
28,Templates,Use key with v-for,Proper list rendering and updates,Unique key for each item,Index as key for dynamic lists,"v-for=""item in items"" :key=""item.id""","v-for=""(item, i) in items"" :key=""i""",High, 28,Templates,Use key with v-for,Proper list rendering and updates,Unique key for each item,Index as key for dynamic lists,"v-for=""item in items"" :key=""item.id""","v-for=""(item, i) in items"" :key=""i""",High,
29,State,Use Pinia for global state,Official state management for Vue 3,Pinia stores for shared state,Vuex for new projects,const store = useCounterStore(),Vuex with mutations,Medium,https://pinia.vuejs.org/ 29,State,Use Pinia for global state,Official state management for Vue 3,Pinia stores for shared state,Vuex for new projects,const store = useCounterStore(),Vuex with mutations,Medium,https://pinia.vuejs.org/
30,State,Define stores with defineStore,Composition API style stores,Setup stores with defineStore,Options stores for complex state,"defineStore('counter', () => {})","defineStore('counter', { state })",Low, 30,State,Define stores with defineStore,Composition API style stores,Setup stores with defineStore,Options stores for complex state,"defineStore('counter', () => {})","defineStore('counter', { state })",Low,
31,State,Use storeToRefs for destructuring,Maintain reactivity when destructuring,storeToRefs(store),Direct destructuring,const { count } = storeToRefs(store),const { count } = store,High,https://pinia.vuejs.org/core-concepts/#destructuring-from-a-store 31,State,Use storeToRefs for destructuring,Maintain reactivity when destructuring,storeToRefs(store),Direct destructuring,const { count } = storeToRefs(store),const { count } = store,High,https://pinia.vuejs.org/core-concepts/#destructuring-from-a-store
32,Routing,Use useRouter and useRoute,Composition API router access,useRouter() useRoute() in setup,this.$router this.$route,const router = useRouter(),this.$router.push(),Medium,https://router.vuejs.org/guide/advanced/composition-api.html 32,Routing,Use useRouter and useRoute,Composition API router access,useRouter() useRoute() in setup,this.$router this.$route,const router = useRouter(),this.$router.push(),Medium,https://router.vuejs.org/guide/advanced/composition-api.html
33,Routing,Lazy load route components,Code splitting for routes,() => import() for components,Static imports for all routes,component: () => import('./Page.vue'),component: Page,Medium,https://router.vuejs.org/guide/advanced/lazy-loading.html 33,Routing,Lazy load route components,Code splitting for routes,() => import() for components,Static imports for all routes,component: () => import('./Page.vue'),component: Page,Medium,https://router.vuejs.org/guide/advanced/lazy-loading.html
34,Routing,Use navigation guards,Protect routes and handle redirects,beforeEach for auth checks,Check auth in each component,router.beforeEach((to) => {}),Check auth in onMounted,Medium, 34,Routing,Use navigation guards,Protect routes and handle redirects,beforeEach for auth checks,Check auth in each component,router.beforeEach((to) => {}),Check auth in onMounted,Medium,
35,Performance,Use v-once for static content,Skip re-renders for static elements,v-once on never-changing content,v-once on dynamic content,<div v-once>{{ staticText }}</div>,<div v-once>{{ dynamicText }}</div>,Low,https://vuejs.org/api/built-in-directives.html#v-once 35,Performance,Use v-once for static content,Skip re-renders for static elements,v-once on never-changing content,v-once on dynamic content,<div v-once>{{ staticText }}</div>,<div v-once>{{ dynamicText }}</div>,Low,https://vuejs.org/api/built-in-directives.html#v-once
36,Performance,Use v-memo for expensive lists,Memoize list items,v-memo with dependency array,Re-render entire list always,"<div v-for v-memo=""[item.id]"">",<div v-for> without memo,Medium,https://vuejs.org/api/built-in-directives.html#v-memo 36,Performance,Use v-memo for expensive lists,Memoize list items,v-memo with dependency array,Re-render entire list always,"<div v-for v-memo=""[item.id]"">",<div v-for> without memo,Medium,https://vuejs.org/api/built-in-directives.html#v-memo
37,Performance,Use shallowReactive for flat objects,Avoid deep reactivity overhead,shallowReactive for flat state,reactive for simple objects,shallowReactive({ count: 0 }),reactive({ count: 0 }),Low, 37,Performance,Use shallowReactive for flat objects,Avoid deep reactivity overhead,shallowReactive for flat state,reactive for simple objects,shallowReactive({ count: 0 }),reactive({ count: 0 }),Low,
38,Performance,Use defineAsyncComponent,Lazy load heavy components,defineAsyncComponent for modals dialogs,Import all components eagerly,defineAsyncComponent(() => import()),import HeavyComponent from,Medium,https://vuejs.org/guide/components/async.html 38,Performance,Use defineAsyncComponent,Lazy load heavy components,defineAsyncComponent for modals dialogs,Import all components eagerly,defineAsyncComponent(() => import()),import HeavyComponent from,Medium,https://vuejs.org/guide/components/async.html
39,TypeScript,Use generic components,Type-safe reusable components,Generic with defineComponent,Any types in components,"<script setup lang=""ts"" generic=""T"">",<script setup> without types,Medium,https://vuejs.org/guide/typescript/composition-api.html 39,TypeScript,Use generic components,Type-safe reusable components,Generic with defineComponent,Any types in components,"<script setup lang=""ts"" generic=""T"">",<script setup> without types,Medium,https://vuejs.org/guide/typescript/composition-api.html
40,TypeScript,Type template refs,Proper typing for DOM refs,ref<HTMLInputElement>(null),ref(null) without type,const input = ref<HTMLInputElement>(null),const input = ref(null),Medium, 40,TypeScript,Type template refs,Proper typing for DOM refs,ref<HTMLInputElement>(null),ref(null) without type,const input = ref<HTMLInputElement>(null),const input = ref(null),Medium,
41,TypeScript,Use PropType for complex props,Type complex prop types,PropType<User> for object props,Object without type,type: Object as PropType<User>,type: Object,Medium, 41,TypeScript,Use PropType for complex props,Type complex prop types,PropType<User> for object props,Object without type,type: Object as PropType<User>,type: Object,Medium,
42,Testing,Use Vue Test Utils,Official testing library,mount shallowMount for components,Manual DOM testing,import { mount } from '@vue/test-utils',document.createElement,Medium,https://test-utils.vuejs.org/ 42,Testing,Use Vue Test Utils,Official testing library,mount shallowMount for components,Manual DOM testing,import { mount } from '@vue/test-utils',document.createElement,Medium,https://test-utils.vuejs.org/
43,Testing,Test component behavior,Focus on inputs and outputs,Test props emit and rendered output,Test internal implementation,expect(wrapper.text()).toContain(),expect(wrapper.vm.internalState),Medium, 43,Testing,Test component behavior,Focus on inputs and outputs,Test props emit and rendered output,Test internal implementation,expect(wrapper.text()).toContain(),expect(wrapper.vm.internalState),Medium,
44,Forms,Use v-model modifiers,Built-in input handling,.lazy .number .trim modifiers,Manual input parsing,"<input v-model.number=""age"">","<input v-model=""age""> then parse",Low,https://vuejs.org/guide/essentials/forms.html#modifiers 44,Forms,Use v-model modifiers,Built-in input handling,.lazy .number .trim modifiers,Manual input parsing,"<input v-model.number=""age"">","<input v-model=""age""> then parse",Low,https://vuejs.org/guide/essentials/forms.html#modifiers
45,Forms,Use VeeValidate or FormKit,Form validation libraries,VeeValidate for complex forms,Manual validation logic,useField useForm from vee-validate,Custom validation in each input,Medium, 45,Forms,Use VeeValidate or FormKit,Form validation libraries,VeeValidate for complex forms,Manual validation logic,useField useForm from vee-validate,Custom validation in each input,Medium,
46,Accessibility,Use semantic elements,Proper HTML elements in templates,button nav main for purpose,div for everything,<button @click>,<div @click>,High, 46,Accessibility,Use semantic elements,Proper HTML elements in templates,button nav main for purpose,div for everything,<button @click>,<div @click>,High,
47,Accessibility,Bind aria attributes dynamically,Keep ARIA in sync with state,":aria-expanded=""isOpen""",Static ARIA values,":aria-expanded=""menuOpen""","aria-expanded=""true""",Medium, 47,Accessibility,Bind aria attributes dynamically,Keep ARIA in sync with state,":aria-expanded=""isOpen""",Static ARIA values,":aria-expanded=""menuOpen""","aria-expanded=""true""",Medium,
48,SSR,Use Nuxt for SSR,Full-featured SSR framework,Nuxt 3 for SSR apps,Manual SSR setup,npx nuxi init my-app,Custom SSR configuration,Medium,https://nuxt.com/ 48,SSR,Use Nuxt for SSR,Full-featured SSR framework,Nuxt 3 for SSR apps,Manual SSR setup,npx nuxi init my-app,Custom SSR configuration,Medium,https://nuxt.com/
49,SSR,Handle hydration mismatches,Client/server content must match,ClientOnly for browser-only content,Different content server/client,<ClientOnly><BrowserWidget/></ClientOnly>,<div>{{ Date.now() }}</div>,High, 49,SSR,Handle hydration mismatches,Client/server content must match,ClientOnly for browser-only content,Different content server/client,<ClientOnly><BrowserWidget/></ClientOnly>,<div>{{ Date.now() }}</div>,High,
1 No Category Guideline Description Do Don't Code Good Code Bad Severity Docs URL
2 1 Composition Use Composition API for new projects Composition API offers better TypeScript support and logic reuse <script setup> for components Options API for new projects <script setup> export default { data() } Medium https://vuejs.org/guide/extras/composition-api-faq.html
3 2 Composition Use script setup syntax Cleaner syntax with automatic exports <script setup> with defineProps setup() function manually <script setup> <script> setup() { return {} } Low https://vuejs.org/api/sfc-script-setup.html
4 3 Reactivity Use ref for primitives ref() for primitive values that need reactivity ref() for strings numbers booleans reactive() for primitives const count = ref(0) const count = reactive(0) Medium https://vuejs.org/guide/essentials/reactivity-fundamentals.html
5 4 Reactivity Use reactive for objects reactive() for complex objects and arrays reactive() for objects with multiple properties ref() for complex objects const state = reactive({ user: null }) const state = ref({ user: null }) Medium
6 5 Reactivity Access ref values with .value Remember .value in script unwrap in template Use .value in script Forget .value in script count.value++ count++ (in script) High
7 6 Reactivity Use computed for derived state Computed properties cache and update automatically computed() for derived values Methods for derived values const doubled = computed(() => count.value * 2) const doubled = () => count.value * 2 Medium https://vuejs.org/guide/essentials/computed.html
8 7 Reactivity Use shallowRef for large objects Avoid deep reactivity for performance shallowRef for large data structures ref for large nested objects const bigData = shallowRef(largeObject) const bigData = ref(largeObject) Medium https://vuejs.org/api/reactivity-advanced.html#shallowref
9 8 Watchers Use watchEffect for simple cases Auto-tracks dependencies watchEffect for simple reactive effects watch with explicit deps when not needed watchEffect(() => console.log(count.value)) watch(count, (val) => console.log(val)) Low https://vuejs.org/guide/essentials/watchers.html
10 9 Watchers Use watch for specific sources Explicit control over what to watch watch with specific refs watchEffect for complex conditional logic watch(userId, fetchUser) watchEffect with conditionals Medium
11 10 Watchers Clean up side effects Return cleanup function in watchers Return cleanup in watchEffect Leave subscriptions open watchEffect((onCleanup) => { onCleanup(unsub) }) watchEffect without cleanup High
12 11 Props Define props with defineProps Type-safe prop definitions defineProps with TypeScript Props without types defineProps<{ msg: string }>() defineProps(['msg']) Medium https://vuejs.org/guide/typescript/composition-api.html#typing-component-props
13 12 Props Use withDefaults for default values Provide defaults for optional props withDefaults with defineProps Defaults in destructuring withDefaults(defineProps<Props>(), { count: 0 }) const { count = 0 } = defineProps() Medium
14 13 Props Avoid mutating props Props should be read-only Emit events to parent for changes Direct prop mutation emit('update:modelValue', newVal) props.modelValue = newVal High
15 14 Emits Define emits with defineEmits Type-safe event emissions defineEmits with types Emit without definition defineEmits<{ change: [id: number] }>() emit('change', id) without define Medium https://vuejs.org/guide/typescript/composition-api.html#typing-component-emits
16 15 Emits Use v-model for two-way binding Simplified parent-child data flow v-model with modelValue prop :value + @input manually <Child v-model="value"/> <Child :value="value" @input="value = $event"/> Low https://vuejs.org/guide/components/v-model.html
17 16 Lifecycle Use onMounted for DOM access DOM is ready in onMounted onMounted for DOM operations Access DOM in setup directly onMounted(() => el.value.focus()) el.value.focus() in setup High https://vuejs.org/api/composition-api-lifecycle.html
18 17 Lifecycle Clean up in onUnmounted Remove listeners and subscriptions onUnmounted for cleanup Leave listeners attached onUnmounted(() => window.removeEventListener()) No cleanup on unmount High
19 18 Lifecycle Avoid onBeforeMount for data Use onMounted or setup for data fetching Fetch in onMounted or setup Fetch in onBeforeMount onMounted(async () => await fetchData()) onBeforeMount(async () => await fetchData()) Low
20 19 Components Use single-file components Keep template script style together .vue files for components Separate template/script files Component.vue with all parts Component.js + Component.html Low
21 20 Components Use PascalCase for components Consistent component naming PascalCase in imports and templates kebab-case in script <MyComponent/> <my-component/> Low https://vuejs.org/style-guide/rules-strongly-recommended.html
22 21 Components Prefer composition over mixins Composables replace mixins Composables for shared logic Mixins for code reuse const { data } = useApi() mixins: [apiMixin] Medium
23 22 Composables Name composables with use prefix Convention for composable functions useFetch useAuth useForm getData or fetchApi export function useFetch() export function fetchData() Medium https://vuejs.org/guide/reusability/composables.html
24 23 Composables Return refs from composables Maintain reactivity when destructuring Return ref values Return reactive objects that lose reactivity return { data: ref(null) } return reactive({ data: null }) Medium
25 24 Composables Accept ref or value params Use toValue for flexible inputs toValue() or unref() for params Only accept ref or only value const val = toValue(maybeRef) const val = maybeRef.value Low https://vuejs.org/api/reactivity-utilities.html#tovalue
26 25 Templates Use v-bind shorthand Cleaner template syntax :prop instead of v-bind:prop Full v-bind syntax <div :class="cls"> <div v-bind:class="cls"> Low
27 26 Templates Use v-on shorthand Cleaner event binding @event instead of v-on:event Full v-on syntax <button @click="handler"> <button v-on:click="handler"> Low
28 27 Templates Avoid v-if with v-for v-if has higher priority causes issues Wrap in template or computed filter v-if on same element as v-for <template v-for><div v-if> <div v-for v-if> High https://vuejs.org/style-guide/rules-essential.html#avoid-v-if-with-v-for
29 28 Templates Use key with v-for Proper list rendering and updates Unique key for each item Index as key for dynamic lists v-for="item in items" :key="item.id" v-for="(item, i) in items" :key="i" High
30 29 State Use Pinia for global state Official state management for Vue 3 Pinia stores for shared state Vuex for new projects const store = useCounterStore() Vuex with mutations Medium https://pinia.vuejs.org/
31 30 State Define stores with defineStore Composition API style stores Setup stores with defineStore Options stores for complex state defineStore('counter', () => {}) defineStore('counter', { state }) Low
32 31 State Use storeToRefs for destructuring Maintain reactivity when destructuring storeToRefs(store) Direct destructuring const { count } = storeToRefs(store) const { count } = store High https://pinia.vuejs.org/core-concepts/#destructuring-from-a-store
33 32 Routing Use useRouter and useRoute Composition API router access useRouter() useRoute() in setup this.$router this.$route const router = useRouter() this.$router.push() Medium https://router.vuejs.org/guide/advanced/composition-api.html
34 33 Routing Lazy load route components Code splitting for routes () => import() for components Static imports for all routes component: () => import('./Page.vue') component: Page Medium https://router.vuejs.org/guide/advanced/lazy-loading.html
35 34 Routing Use navigation guards Protect routes and handle redirects beforeEach for auth checks Check auth in each component router.beforeEach((to) => {}) Check auth in onMounted Medium
36 35 Performance Use v-once for static content Skip re-renders for static elements v-once on never-changing content v-once on dynamic content <div v-once>{{ staticText }}</div> <div v-once>{{ dynamicText }}</div> Low https://vuejs.org/api/built-in-directives.html#v-once
37 36 Performance Use v-memo for expensive lists Memoize list items v-memo with dependency array Re-render entire list always <div v-for v-memo="[item.id]"> <div v-for> without memo Medium https://vuejs.org/api/built-in-directives.html#v-memo
38 37 Performance Use shallowReactive for flat objects Avoid deep reactivity overhead shallowReactive for flat state reactive for simple objects shallowReactive({ count: 0 }) reactive({ count: 0 }) Low
39 38 Performance Use defineAsyncComponent Lazy load heavy components defineAsyncComponent for modals dialogs Import all components eagerly defineAsyncComponent(() => import()) import HeavyComponent from Medium https://vuejs.org/guide/components/async.html
40 39 TypeScript Use generic components Type-safe reusable components Generic with defineComponent Any types in components <script setup lang="ts" generic="T"> <script setup> without types Medium https://vuejs.org/guide/typescript/composition-api.html
41 40 TypeScript Type template refs Proper typing for DOM refs ref<HTMLInputElement>(null) ref(null) without type const input = ref<HTMLInputElement>(null) const input = ref(null) Medium
42 41 TypeScript Use PropType for complex props Type complex prop types PropType<User> for object props Object without type type: Object as PropType<User> type: Object Medium
43 42 Testing Use Vue Test Utils Official testing library mount shallowMount for components Manual DOM testing import { mount } from '@vue/test-utils' document.createElement Medium https://test-utils.vuejs.org/
44 43 Testing Test component behavior Focus on inputs and outputs Test props emit and rendered output Test internal implementation expect(wrapper.text()).toContain() expect(wrapper.vm.internalState) Medium
45 44 Forms Use v-model modifiers Built-in input handling .lazy .number .trim modifiers Manual input parsing <input v-model.number="age"> <input v-model="age"> then parse Low https://vuejs.org/guide/essentials/forms.html#modifiers
46 45 Forms Use VeeValidate or FormKit Form validation libraries VeeValidate for complex forms Manual validation logic useField useForm from vee-validate Custom validation in each input Medium
47 46 Accessibility Use semantic elements Proper HTML elements in templates button nav main for purpose div for everything <button @click> <div @click> High
48 47 Accessibility Bind aria attributes dynamically Keep ARIA in sync with state :aria-expanded="isOpen" Static ARIA values :aria-expanded="menuOpen" aria-expanded="true" Medium
49 48 SSR Use Nuxt for SSR Full-featured SSR framework Nuxt 3 for SSR apps Manual SSR setup npx nuxi init my-app Custom SSR configuration Medium https://nuxt.com/
50 49 SSR Handle hydration mismatches Client/server content must match ClientOnly for browser-only content Different content server/client <ClientOnly><BrowserWidget/></ClientOnly> <div>{{ Date.now() }}</div> High

View File

@@ -1,59 +1,59 @@
STT,Style Category,Type,Keywords,Primary Colors,Secondary Colors,Effects & Animation,Best For,Do Not Use For,Light Mode ✓,Dark Mode ✓,Performance,Accessibility,Mobile-Friendly,Conversion-Focused,Framework Compatibility,Era/Origin,Complexity STT,Style Category,Type,Keywords,Primary Colors,Secondary Colors,Effects & Animation,Best For,Do Not Use For,Light Mode ✓,Dark Mode ✓,Performance,Accessibility,Mobile-Friendly,Conversion-Focused,Framework Compatibility,Era/Origin,Complexity
1,Minimalism & Swiss Style,General,"Clean, simple, spacious, functional, white space, high contrast, geometric, sans-serif, grid-based, essential","Monochromatic, Black #000000, White #FFFFFF","Neutral (Beige #F5F1E8, Grey #808080, Taupe #B38B6D), Primary accent","Subtle hover (200-250ms), smooth transitions, sharp shadows if any, clear type hierarchy, fast loading","Enterprise apps, dashboards, documentation sites, SaaS platforms, professional tools","Creative portfolios, entertainment, playful brands, artistic experiments",✓ Full,✓ Full,⚡ Excellent,✓ WCAG AAA,✓ High,◐ Medium,"Tailwind 10/10, Bootstrap 9/10, MUI 9/10",1950s Swiss,Low 1,Minimalism & Swiss Style,General,"Clean, simple, spacious, functional, white space, high contrast, geometric, sans-serif, grid-based, essential","Monochromatic, Black #000000, White #FFFFFF","Neutral (Beige #F5F1E8, Grey #808080, Taupe #B38B6D), Primary accent","Subtle hover (200-250ms), smooth transitions, sharp shadows if any, clear type hierarchy, fast loading","Enterprise apps, dashboards, documentation sites, SaaS platforms, professional tools","Creative portfolios, entertainment, playful brands, artistic experiments",✓ Full,✓ Full,⚡ Excellent,✓ WCAG AAA,✓ High,◐ Medium,"Tailwind 10/10, Bootstrap 9/10, MUI 9/10",1950s Swiss,Low
2,Neumorphism,General,"Soft UI, embossed, debossed, convex, concave, light source, subtle depth, rounded (12-16px), monochromatic","Light pastels: Soft Blue #C8E0F4, Soft Pink #F5E0E8, Soft Grey #E8E8E8","Tints/shades (±30%), gradient subtlety, color harmony","Soft box-shadow (multiple: -5px -5px 15px, 5px 5px 15px), smooth press (150ms), inner subtle shadow","Health/wellness apps, meditation platforms, fitness trackers, minimal interaction UIs","Complex apps, critical accessibility, data-heavy dashboards, high-contrast required",✓ Full,◐ Partial,⚡ Good,⚠ Low contrast,✓ Good,◐ Medium,"Tailwind 8/10, CSS-in-JS 9/10",2020s Modern,Medium 2,Neumorphism,General,"Soft UI, embossed, debossed, convex, concave, light source, subtle depth, rounded (12-16px), monochromatic","Light pastels: Soft Blue #C8E0F4, Soft Pink #F5E0E8, Soft Grey #E8E8E8","Tints/shades (±30%), gradient subtlety, color harmony","Soft box-shadow (multiple: -5px -5px 15px, 5px 5px 15px), smooth press (150ms), inner subtle shadow","Health/wellness apps, meditation platforms, fitness trackers, minimal interaction UIs","Complex apps, critical accessibility, data-heavy dashboards, high-contrast required",✓ Full,◐ Partial,⚡ Good,⚠ Low contrast,✓ Good,◐ Medium,"Tailwind 8/10, CSS-in-JS 9/10",2020s Modern,Medium
3,Glassmorphism,General,"Frosted glass, transparent, blurred background, layered, vibrant background, light source, depth, multi-layer","Translucent white: rgba(255,255,255,0.1-0.3)","Vibrant: Electric Blue #0080FF, Neon Purple #8B00FF, Vivid Pink #FF1493, Teal #20B2AA","Backdrop blur (10-20px), subtle border (1px solid rgba white 0.2), light reflection, Z-depth","Modern SaaS, financial dashboards, high-end corporate, lifestyle apps, modal overlays, navigation","Low-contrast backgrounds, critical accessibility, performance-limited, dark text on dark",✓ Full,✓ Full,⚠ Good,⚠ Ensure 4.5:1,✓ Good,✓ High,"Tailwind 9/10, MUI 8/10, Chakra 8/10",2020s Modern,Medium 3,Glassmorphism,General,"Frosted glass, transparent, blurred background, layered, vibrant background, light source, depth, multi-layer","Translucent white: rgba(255,255,255,0.1-0.3)","Vibrant: Electric Blue #0080FF, Neon Purple #8B00FF, Vivid Pink #FF1493, Teal #20B2AA","Backdrop blur (10-20px), subtle border (1px solid rgba white 0.2), light reflection, Z-depth","Modern SaaS, financial dashboards, high-end corporate, lifestyle apps, modal overlays, navigation","Low-contrast backgrounds, critical accessibility, performance-limited, dark text on dark",✓ Full,✓ Full,⚠ Good,⚠ Ensure 4.5:1,✓ Good,✓ High,"Tailwind 9/10, MUI 8/10, Chakra 8/10",2020s Modern,Medium
4,Brutalism,General,"Raw, unpolished, stark, high contrast, plain text, default fonts, visible borders, asymmetric, anti-design","Primary: Red #FF0000, Blue #0000FF, Yellow #FFFF00, Black #000000, White #FFFFFF","Limited: Neon Green #00FF00, Hot Pink #FF00FF, minimal secondary","No smooth transitions (instant), sharp corners (0px), bold typography (700+), visible grid, large blocks","Design portfolios, artistic projects, counter-culture brands, editorial/media sites, tech blogs","Corporate environments, conservative industries, critical accessibility, customer-facing professional",✓ Full,✓ Full,⚡ Excellent,✓ WCAG AAA,◐ Medium,✗ Low,"Tailwind 10/10, Bootstrap 7/10",1950s Brutalist,Low 4,Brutalism,General,"Raw, unpolished, stark, high contrast, plain text, default fonts, visible borders, asymmetric, anti-design","Primary: Red #FF0000, Blue #0000FF, Yellow #FFFF00, Black #000000, White #FFFFFF","Limited: Neon Green #00FF00, Hot Pink #FF00FF, minimal secondary","No smooth transitions (instant), sharp corners (0px), bold typography (700+), visible grid, large blocks","Design portfolios, artistic projects, counter-culture brands, editorial/media sites, tech blogs","Corporate environments, conservative industries, critical accessibility, customer-facing professional",✓ Full,✓ Full,⚡ Excellent,✓ WCAG AAA,◐ Medium,✗ Low,"Tailwind 10/10, Bootstrap 7/10",1950s Brutalist,Low
5,3D & Hyperrealism,General,"Depth, realistic textures, 3D models, spatial navigation, tactile, skeuomorphic elements, rich detail, immersive","Deep Navy #001F3F, Forest Green #228B22, Burgundy #800020, Gold #FFD700, Silver #C0C0C0","Complex gradients (5-10 stops), realistic lighting, shadow variations (20-40% darker)","WebGL/Three.js 3D, realistic shadows (layers), physics lighting, parallax (3-5 layers), smooth 3D (300-400ms)","Gaming, product showcase, immersive experiences, high-end e-commerce, architectural viz, VR/AR","Low-end mobile, performance-limited, critical accessibility, data tables/forms",◐ Partial,◐ Partial,❌ Poor,⚠ Not accessible,✗ Low,◐ Medium,"Three.js 10/10, R3F 10/10, Babylon.js 10/10",2020s Modern,High 5,3D & Hyperrealism,General,"Depth, realistic textures, 3D models, spatial navigation, tactile, skeuomorphic elements, rich detail, immersive","Deep Navy #001F3F, Forest Green #228B22, Burgundy #800020, Gold #FFD700, Silver #C0C0C0","Complex gradients (5-10 stops), realistic lighting, shadow variations (20-40% darker)","WebGL/Three.js 3D, realistic shadows (layers), physics lighting, parallax (3-5 layers), smooth 3D (300-400ms)","Gaming, product showcase, immersive experiences, high-end e-commerce, architectural viz, VR/AR","Low-end mobile, performance-limited, critical accessibility, data tables/forms",◐ Partial,◐ Partial,❌ Poor,⚠ Not accessible,✗ Low,◐ Medium,"Three.js 10/10, R3F 10/10, Babylon.js 10/10",2020s Modern,High
6,Vibrant & Block-based,General,"Bold, energetic, playful, block layout, geometric shapes, high color contrast, duotone, modern, energetic","Neon Green #39FF14, Electric Purple #BF00FF, Vivid Pink #FF1493, Bright Cyan #00FFFF, Sunburst #FFAA00","Complementary: Orange #FF7F00, Shocking Pink #FF006E, Lime #CCFF00, triadic schemes","Large sections (48px+ gaps), animated patterns, bold hover (color shift), scroll-snap, large type (32px+), 200-300ms","Startups, creative agencies, gaming, social media, youth-focused, entertainment, consumer","Financial institutions, healthcare, formal business, government, conservative, elderly",✓ Full,✓ Full,⚡ Good,◐ Ensure WCAG,✓ High,✓ High,"Tailwind 10/10, Chakra 9/10, Styled 9/10",2020s Modern,Medium 6,Vibrant & Block-based,General,"Bold, energetic, playful, block layout, geometric shapes, high color contrast, duotone, modern, energetic","Neon Green #39FF14, Electric Purple #BF00FF, Vivid Pink #FF1493, Bright Cyan #00FFFF, Sunburst #FFAA00","Complementary: Orange #FF7F00, Shocking Pink #FF006E, Lime #CCFF00, triadic schemes","Large sections (48px+ gaps), animated patterns, bold hover (color shift), scroll-snap, large type (32px+), 200-300ms","Startups, creative agencies, gaming, social media, youth-focused, entertainment, consumer","Financial institutions, healthcare, formal business, government, conservative, elderly",✓ Full,✓ Full,⚡ Good,◐ Ensure WCAG,✓ High,✓ High,"Tailwind 10/10, Chakra 9/10, Styled 9/10",2020s Modern,Medium
7,Dark Mode (OLED),General,"Dark theme, low light, high contrast, deep black, midnight blue, eye-friendly, OLED, night mode, power efficient","Deep Black #000000, Dark Grey #121212, Midnight Blue #0A0E27","Vibrant accents: Neon Green #39FF14, Electric Blue #0080FF, Gold #FFD700, Plasma Purple #BF00FF","Minimal glow (text-shadow: 0 0 10px), dark-to-light transitions, low white emission, high readability, visible focus","Night-mode apps, coding platforms, entertainment, eye-strain prevention, OLED devices, low-light","Print-first content, high-brightness outdoor, color-accuracy-critical",✗ No,✓ Only,⚡ Excellent,✓ WCAG AAA,✓ High,◐ Low,"Tailwind 10/10, MUI 10/10, Chakra 10/10",2020s Modern,Low 7,Dark Mode (OLED),General,"Dark theme, low light, high contrast, deep black, midnight blue, eye-friendly, OLED, night mode, power efficient","Deep Black #000000, Dark Grey #121212, Midnight Blue #0A0E27","Vibrant accents: Neon Green #39FF14, Electric Blue #0080FF, Gold #FFD700, Plasma Purple #BF00FF","Minimal glow (text-shadow: 0 0 10px), dark-to-light transitions, low white emission, high readability, visible focus","Night-mode apps, coding platforms, entertainment, eye-strain prevention, OLED devices, low-light","Print-first content, high-brightness outdoor, color-accuracy-critical",✗ No,✓ Only,⚡ Excellent,✓ WCAG AAA,✓ High,◐ Low,"Tailwind 10/10, MUI 10/10, Chakra 10/10",2020s Modern,Low
8,Accessible & Ethical,General,"High contrast, large text (16px+), keyboard navigation, screen reader friendly, WCAG compliant, focus state, semantic","WCAG AA/AAA (4.5:1 min), simple primary, clear secondary, high luminosity (7:1+)","Symbol-based colors (not color-only), supporting patterns, inclusive combinations","Clear focus rings (3-4px), ARIA labels, skip links, responsive design, reduced motion, 44x44px touch targets","Government, healthcare, education, inclusive products, large audience, legal compliance, public",None - accessibility universal,✓ Full,✓ Full,⚡ Excellent,✓ WCAG AAA,✓ High,✓ High,"All frameworks 10/10",Universal,Low 8,Accessible & Ethical,General,"High contrast, large text (16px+), keyboard navigation, screen reader friendly, WCAG compliant, focus state, semantic","WCAG AA/AAA (4.5:1 min), simple primary, clear secondary, high luminosity (7:1+)","Symbol-based colors (not color-only), supporting patterns, inclusive combinations","Clear focus rings (3-4px), ARIA labels, skip links, responsive design, reduced motion, 44x44px touch targets","Government, healthcare, education, inclusive products, large audience, legal compliance, public",None - accessibility universal,✓ Full,✓ Full,⚡ Excellent,✓ WCAG AAA,✓ High,✓ High,"All frameworks 10/10",Universal,Low
9,Claymorphism,General,"Soft 3D, chunky, playful, toy-like, bubbly, thick borders (3-4px), double shadows, rounded (16-24px)","Pastel: Soft Peach #FDBCB4, Baby Blue #ADD8E6, Mint #98FF98, Lilac #E6E6FA, light BG","Soft gradients (pastel-to-pastel), light/dark variations (20-30%), gradient subtle","Inner+outer shadows (subtle, no hard lines), soft press (200ms ease-out), fluffy elements, smooth transitions","Educational apps, children's apps, SaaS platforms, creative tools, fun-focused, onboarding, casual games","Formal corporate, professional services, data-critical, serious/medical, legal apps, finance",✓ Full,◐ Partial,⚡ Good,⚠ Ensure 4.5:1,✓ High,✓ High,"Tailwind 9/10, CSS-in-JS 9/10",2020s Modern,Medium 9,Claymorphism,General,"Soft 3D, chunky, playful, toy-like, bubbly, thick borders (3-4px), double shadows, rounded (16-24px)","Pastel: Soft Peach #FDBCB4, Baby Blue #ADD8E6, Mint #98FF98, Lilac #E6E6FA, light BG","Soft gradients (pastel-to-pastel), light/dark variations (20-30%), gradient subtle","Inner+outer shadows (subtle, no hard lines), soft press (200ms ease-out), fluffy elements, smooth transitions","Educational apps, children's apps, SaaS platforms, creative tools, fun-focused, onboarding, casual games","Formal corporate, professional services, data-critical, serious/medical, legal apps, finance",✓ Full,◐ Partial,⚡ Good,⚠ Ensure 4.5:1,✓ High,✓ High,"Tailwind 9/10, CSS-in-JS 9/10",2020s Modern,Medium
10,Aurora UI,General,"Vibrant gradients, smooth blend, Northern Lights effect, mesh gradient, luminous, atmospheric, abstract","Complementary: Blue-Orange, Purple-Yellow, Electric Blue #0080FF, Magenta #FF1493, Cyan #00FFFF","Smooth transitions (Blue→Purple→Pink→Teal), iridescent effects, blend modes (screen, multiply)","Large flowing CSS/SVG gradients, subtle 8-12s animations, depth via color layering, smooth morph","Modern SaaS, creative agencies, branding, music platforms, lifestyle, premium products, hero sections","Data-heavy dashboards, critical accessibility, content-heavy where distraction issues",✓ Full,✓ Full,⚠ Good,⚠ Text contrast,✓ Good,✓ High,"Tailwind 9/10, CSS-in-JS 10/10",2020s Modern,Medium 10,Aurora UI,General,"Vibrant gradients, smooth blend, Northern Lights effect, mesh gradient, luminous, atmospheric, abstract","Complementary: Blue-Orange, Purple-Yellow, Electric Blue #0080FF, Magenta #FF1493, Cyan #00FFFF","Smooth transitions (Blue→Purple→Pink→Teal), iridescent effects, blend modes (screen, multiply)","Large flowing CSS/SVG gradients, subtle 8-12s animations, depth via color layering, smooth morph","Modern SaaS, creative agencies, branding, music platforms, lifestyle, premium products, hero sections","Data-heavy dashboards, critical accessibility, content-heavy where distraction issues",✓ Full,✓ Full,⚠ Good,⚠ Text contrast,✓ Good,✓ High,"Tailwind 9/10, CSS-in-JS 10/10",2020s Modern,Medium
11,Retro-Futurism,General,"Vintage sci-fi, 80s aesthetic, neon glow, geometric patterns, CRT scanlines, pixel art, cyberpunk, synthwave","Neon Blue #0080FF, Hot Pink #FF006E, Cyan #00FFFF, Deep Black #1A1A2E, Purple #5D34D0","Metallic Silver #C0C0C0, Gold #FFD700, duotone, 80s Pink #FF10F0, neon accents","CRT scanlines (::before overlay), neon glow (text-shadow+box-shadow), glitch effects (skew/offset keyframes)","Gaming, entertainment, music platforms, tech brands, artistic projects, nostalgic, cyberpunk","Conservative industries, critical accessibility, professional/corporate, elderly, legal/finance",✓ Full,✓ Dark focused,⚠ Moderate,⚠ High contrast/strain,◐ Medium,◐ Medium,"Tailwind 8/10, CSS-in-JS 9/10",1980s Retro,Medium 11,Retro-Futurism,General,"Vintage sci-fi, 80s aesthetic, neon glow, geometric patterns, CRT scanlines, pixel art, cyberpunk, synthwave","Neon Blue #0080FF, Hot Pink #FF006E, Cyan #00FFFF, Deep Black #1A1A2E, Purple #5D34D0","Metallic Silver #C0C0C0, Gold #FFD700, duotone, 80s Pink #FF10F0, neon accents","CRT scanlines (::before overlay), neon glow (text-shadow+box-shadow), glitch effects (skew/offset keyframes)","Gaming, entertainment, music platforms, tech brands, artistic projects, nostalgic, cyberpunk","Conservative industries, critical accessibility, professional/corporate, elderly, legal/finance",✓ Full,✓ Dark focused,⚠ Moderate,⚠ High contrast/strain,◐ Medium,◐ Medium,"Tailwind 8/10, CSS-in-JS 9/10",1980s Retro,Medium
12,Flat Design,General,"2D, minimalist, bold colors, no shadows, clean lines, simple shapes, typography-focused, modern, icon-heavy","Solid bright: Red, Orange, Blue, Green, limited palette (4-6 max)","Complementary colors, muted secondaries, high saturation, clean accents","No gradients/shadows, simple hover (color/opacity shift), fast loading, clean transitions (150-200ms ease), minimal icons","Web apps, mobile apps, cross-platform, startup MVPs, user-friendly, SaaS, dashboards, corporate","Complex 3D, premium/luxury, artistic portfolios, immersive experiences, high-detail",✓ Full,✓ Full,⚡ Excellent,✓ WCAG AAA,✓ High,✓ High,"Tailwind 10/10, Bootstrap 10/10, MUI 9/10",2010s Modern,Low 12,Flat Design,General,"2D, minimalist, bold colors, no shadows, clean lines, simple shapes, typography-focused, modern, icon-heavy","Solid bright: Red, Orange, Blue, Green, limited palette (4-6 max)","Complementary colors, muted secondaries, high saturation, clean accents","No gradients/shadows, simple hover (color/opacity shift), fast loading, clean transitions (150-200ms ease), minimal icons","Web apps, mobile apps, cross-platform, startup MVPs, user-friendly, SaaS, dashboards, corporate","Complex 3D, premium/luxury, artistic portfolios, immersive experiences, high-detail",✓ Full,✓ Full,⚡ Excellent,✓ WCAG AAA,✓ High,✓ High,"Tailwind 10/10, Bootstrap 10/10, MUI 9/10",2010s Modern,Low
13,Skeuomorphism,General,"Realistic, texture, depth, 3D appearance, real-world metaphors, shadows, gradients, tactile, detailed, material","Rich realistic: wood, leather, metal colors, detailed gradients (8-12 stops), metallic effects","Realistic lighting gradients, shadow variations (30-50% darker), texture overlays, material colors","Realistic shadows (layers), depth (perspective), texture details (noise, grain), realistic animations (300-500ms)","Legacy apps, gaming, immersive storytelling, premium products, luxury, realistic simulations, education","Modern enterprise, critical accessibility, low-performance, web (use Flat/Modern)",◐ Partial,◐ Partial,❌ Poor,⚠ Textures reduce readability,✗ Low,◐ Medium,"CSS-in-JS 7/10, Custom 8/10",2007-2012 iOS,High 13,Skeuomorphism,General,"Realistic, texture, depth, 3D appearance, real-world metaphors, shadows, gradients, tactile, detailed, material","Rich realistic: wood, leather, metal colors, detailed gradients (8-12 stops), metallic effects","Realistic lighting gradients, shadow variations (30-50% darker), texture overlays, material colors","Realistic shadows (layers), depth (perspective), texture details (noise, grain), realistic animations (300-500ms)","Legacy apps, gaming, immersive storytelling, premium products, luxury, realistic simulations, education","Modern enterprise, critical accessibility, low-performance, web (use Flat/Modern)",◐ Partial,◐ Partial,❌ Poor,⚠ Textures reduce readability,✗ Low,◐ Medium,"CSS-in-JS 7/10, Custom 8/10",2007-2012 iOS,High
14,Liquid Glass,General,"Flowing glass, morphing, smooth transitions, fluid effects, translucent, animated blur, iridescent, chromatic aberration","Vibrant iridescent (rainbow spectrum), translucent base with opacity shifts, gradient fluidity","Chromatic aberration (Red-Cyan), iridescent oil-spill, fluid gradient blends, holographic effects","Morphing elements (SVG/CSS), fluid animations (400-600ms curves), dynamic blur (backdrop-filter), color transitions","Premium SaaS, high-end e-commerce, creative platforms, branding experiences, luxury portfolios","Performance-limited, critical accessibility, complex data, budget projects",✓ Full,✓ Full,⚠ Moderate-Poor,⚠ Text contrast,◐ Medium,✓ High,"Framer Motion 10/10, GSAP 10/10",2020s Modern,High 14,Liquid Glass,General,"Flowing glass, morphing, smooth transitions, fluid effects, translucent, animated blur, iridescent, chromatic aberration","Vibrant iridescent (rainbow spectrum), translucent base with opacity shifts, gradient fluidity","Chromatic aberration (Red-Cyan), iridescent oil-spill, fluid gradient blends, holographic effects","Morphing elements (SVG/CSS), fluid animations (400-600ms curves), dynamic blur (backdrop-filter), color transitions","Premium SaaS, high-end e-commerce, creative platforms, branding experiences, luxury portfolios","Performance-limited, critical accessibility, complex data, budget projects",✓ Full,✓ Full,⚠ Moderate-Poor,⚠ Text contrast,◐ Medium,✓ High,"Framer Motion 10/10, GSAP 10/10",2020s Modern,High
15,Motion-Driven,General,"Animation-heavy, microinteractions, smooth transitions, scroll effects, parallax, entrance anim, page transitions","Bold colors emphasize movement, high contrast animated, dynamic gradients, accent action colors","Transitional states, success (Green #22C55E), error (Red #EF4444), neutral feedback","Scroll anim (Intersection Observer), hover (300-400ms), entrance, parallax (3-5 layers), page transitions","Portfolio sites, storytelling platforms, interactive experiences, entertainment apps, creative, SaaS","Data dashboards, critical accessibility, low-power devices, content-heavy, motion-sensitive",✓ Full,✓ Full,⚠ Good,⚠ Prefers-reduced-motion,✓ Good,✓ High,"GSAP 10/10, Framer Motion 10/10",2020s Modern,High 15,Motion-Driven,General,"Animation-heavy, microinteractions, smooth transitions, scroll effects, parallax, entrance anim, page transitions","Bold colors emphasize movement, high contrast animated, dynamic gradients, accent action colors","Transitional states, success (Green #22C55E), error (Red #EF4444), neutral feedback","Scroll anim (Intersection Observer), hover (300-400ms), entrance, parallax (3-5 layers), page transitions","Portfolio sites, storytelling platforms, interactive experiences, entertainment apps, creative, SaaS","Data dashboards, critical accessibility, low-power devices, content-heavy, motion-sensitive",✓ Full,✓ Full,⚠ Good,⚠ Prefers-reduced-motion,✓ Good,✓ High,"GSAP 10/10, Framer Motion 10/10",2020s Modern,High
16,Micro-interactions,General,"Small animations, gesture-based, tactile feedback, subtle animations, contextual interactions, responsive","Subtle color shifts (10-20%), feedback: Green #22C55E, Red #EF4444, Amber #F59E0B","Accent feedback, neutral supporting, clear action indicators","Small hover (50-100ms), loading spinners, success/error state anim, gesture-triggered (swipe/pinch), haptic","Mobile apps, touchscreen UIs, productivity tools, user-friendly, consumer apps, interactive components","Desktop-only, critical performance, accessibility-first (alternatives needed)",✓ Full,✓ Full,⚡ Excellent,✓ Good,✓ High,✓ High,"Framer Motion 10/10, React Spring 9/10",2020s Modern,Medium 16,Micro-interactions,General,"Small animations, gesture-based, tactile feedback, subtle animations, contextual interactions, responsive","Subtle color shifts (10-20%), feedback: Green #22C55E, Red #EF4444, Amber #F59E0B","Accent feedback, neutral supporting, clear action indicators","Small hover (50-100ms), loading spinners, success/error state anim, gesture-triggered (swipe/pinch), haptic","Mobile apps, touchscreen UIs, productivity tools, user-friendly, consumer apps, interactive components","Desktop-only, critical performance, accessibility-first (alternatives needed)",✓ Full,✓ Full,⚡ Excellent,✓ Good,✓ High,✓ High,"Framer Motion 10/10, React Spring 9/10",2020s Modern,Medium
17,Inclusive Design,General,"Accessible, color-blind friendly, high contrast, haptic feedback, voice interaction, screen reader, WCAG AAA, universal","WCAG AAA (7:1+ contrast), avoid red-green only, symbol-based indicators, high contrast primary","Supporting patterns (stripes, dots, hatch), symbols, combinations, clear non-color indicators","Haptic feedback (vibration), voice guidance, focus indicators (4px+ ring), motion options, alt content, semantic","Public services, education, healthcare, finance, government, accessible consumer, inclusive",None - accessibility universal,✓ Full,✓ Full,⚡ Excellent,✓ WCAG AAA,✓ High,✓ High,"All frameworks 10/10",Universal,Low 17,Inclusive Design,General,"Accessible, color-blind friendly, high contrast, haptic feedback, voice interaction, screen reader, WCAG AAA, universal","WCAG AAA (7:1+ contrast), avoid red-green only, symbol-based indicators, high contrast primary","Supporting patterns (stripes, dots, hatch), symbols, combinations, clear non-color indicators","Haptic feedback (vibration), voice guidance, focus indicators (4px+ ring), motion options, alt content, semantic","Public services, education, healthcare, finance, government, accessible consumer, inclusive",None - accessibility universal,✓ Full,✓ Full,⚡ Excellent,✓ WCAG AAA,✓ High,✓ High,"All frameworks 10/10",Universal,Low
18,Zero Interface,General,"Minimal visible UI, voice-first, gesture-based, AI-driven, invisible controls, predictive, context-aware, ambient","Neutral backgrounds: Soft white #FAFAFA, light grey #F0F0F0, warm off-white #F5F1E8","Subtle feedback: light green, light red, minimal UI elements, soft accents","Voice recognition UI, gesture detection, AI predictions (smooth reveal), progressive disclosure, smart suggestions","Voice assistants, AI platforms, future-forward UX, smart home, contextual computing, ambient experiences","Complex workflows, data-entry heavy, traditional systems, legacy support, explicit control",✓ Full,✓ Full,⚡ Excellent,✓ Excellent,✓ High,✓ High,"Tailwind 10/10, Custom 10/10",2020s AI-Era,Low 18,Zero Interface,General,"Minimal visible UI, voice-first, gesture-based, AI-driven, invisible controls, predictive, context-aware, ambient","Neutral backgrounds: Soft white #FAFAFA, light grey #F0F0F0, warm off-white #F5F1E8","Subtle feedback: light green, light red, minimal UI elements, soft accents","Voice recognition UI, gesture detection, AI predictions (smooth reveal), progressive disclosure, smart suggestions","Voice assistants, AI platforms, future-forward UX, smart home, contextual computing, ambient experiences","Complex workflows, data-entry heavy, traditional systems, legacy support, explicit control",✓ Full,✓ Full,⚡ Excellent,✓ Excellent,✓ High,✓ High,"Tailwind 10/10, Custom 10/10",2020s AI-Era,Low
19,Soft UI Evolution,General,"Evolved soft UI, better contrast, modern aesthetics, subtle depth, accessibility-focused, improved shadows, hybrid","Improved contrast pastels: Soft Blue #87CEEB, Soft Pink #FFB6C1, Soft Green #90EE90, better hierarchy","Better combinations, accessible secondary, supporting with improved contrast, modern accents","Improved shadows (softer than flat, clearer than neumorphism), modern (200-300ms), focus visible, WCAG AA/AAA","Modern enterprise apps, SaaS platforms, health/wellness, modern business tools, professional, hybrid","Extreme minimalism, critical performance, systems without modern OS",✓ Full,✓ Full,⚡ Excellent,✓ WCAG AA+,✓ High,✓ High,"Tailwind 9/10, MUI 9/10, Chakra 9/10",2020s Modern,Medium 19,Soft UI Evolution,General,"Evolved soft UI, better contrast, modern aesthetics, subtle depth, accessibility-focused, improved shadows, hybrid","Improved contrast pastels: Soft Blue #87CEEB, Soft Pink #FFB6C1, Soft Green #90EE90, better hierarchy","Better combinations, accessible secondary, supporting with improved contrast, modern accents","Improved shadows (softer than flat, clearer than neumorphism), modern (200-300ms), focus visible, WCAG AA/AAA","Modern enterprise apps, SaaS platforms, health/wellness, modern business tools, professional, hybrid","Extreme minimalism, critical performance, systems without modern OS",✓ Full,✓ Full,⚡ Excellent,✓ WCAG AA+,✓ High,✓ High,"Tailwind 9/10, MUI 9/10, Chakra 9/10",2020s Modern,Medium
20,Hero-Centric Design,Landing Page,"Large hero section, compelling headline, high-contrast CTA, product showcase, value proposition, hero image/video, dramatic visual","Brand primary color, white/light backgrounds for contrast, accent color for CTA","Supporting colors for secondary CTAs, accent highlights, trust elements (testimonials, logos)","Smooth scroll reveal, fade-in animations on hero, subtle background parallax, CTA glow/pulse effect","SaaS landing pages, product launches, service landing pages, B2B platforms, tech companies","Complex navigation, multi-page experiences, data-heavy applications",✓ Full,✓ Full,⚡ Good,✓ WCAG AA,✓ Full,✓ Very High,"Tailwind 10/10, Bootstrap 9/10",2020s Modern,Medium 20,Hero-Centric Design,Landing Page,"Large hero section, compelling headline, high-contrast CTA, product showcase, value proposition, hero image/video, dramatic visual","Brand primary color, white/light backgrounds for contrast, accent color for CTA","Supporting colors for secondary CTAs, accent highlights, trust elements (testimonials, logos)","Smooth scroll reveal, fade-in animations on hero, subtle background parallax, CTA glow/pulse effect","SaaS landing pages, product launches, service landing pages, B2B platforms, tech companies","Complex navigation, multi-page experiences, data-heavy applications",✓ Full,✓ Full,⚡ Good,✓ WCAG AA,✓ Full,✓ Very High,"Tailwind 10/10, Bootstrap 9/10",2020s Modern,Medium
21,Conversion-Optimized,Landing Page,"Form-focused, minimalist design, single CTA focus, high contrast, urgency elements, trust signals, social proof, clear value","Primary brand color, high-contrast white/light backgrounds, warning/urgency colors for time-limited offers","Secondary CTA color (muted), trust element colors (testimonial highlights), accent for key benefits","Hover states on CTA (color shift, slight scale), form field focus animations, loading spinner, success feedback","E-commerce product pages, free trial signups, lead generation, SaaS pricing pages, limited-time offers","Complex feature explanations, multi-product showcases, technical documentation",✓ Full,✓ Full,⚡ Excellent,✓ WCAG AA,✓ Full (mobile-optimized),✓ Very High 21,Conversion-Optimized,Landing Page,"Form-focused, minimalist design, single CTA focus, high contrast, urgency elements, trust signals, social proof, clear value","Primary brand color, high-contrast white/light backgrounds, warning/urgency colors for time-limited offers","Secondary CTA color (muted), trust element colors (testimonial highlights), accent for key benefits","Hover states on CTA (color shift, slight scale), form field focus animations, loading spinner, success feedback","E-commerce product pages, free trial signups, lead generation, SaaS pricing pages, limited-time offers","Complex feature explanations, multi-product showcases, technical documentation",✓ Full,✓ Full,⚡ Excellent,✓ WCAG AA,✓ Full (mobile-optimized),✓ Very High
22,Feature-Rich Showcase,Landing Page,"Multiple feature sections, grid layout, benefit cards, visual feature demonstrations, interactive elements, problem-solution pairs","Primary brand, bright secondary colors for feature cards, contrasting accent for CTAs","Supporting colors for: benefits (green), problems (red/orange), features (blue/purple), social proof (neutral)","Card hover effects (lift/scale), icon animations on scroll, feature toggle animations, smooth section transitions","Enterprise SaaS, software tools landing pages, platform services, complex product explanations, B2B products","Simple product pages, early-stage startups with few features, entertainment landing pages",✓ Full,✓ Full,⚡ Good,✓ WCAG AA,✓ Good,✓ High 22,Feature-Rich Showcase,Landing Page,"Multiple feature sections, grid layout, benefit cards, visual feature demonstrations, interactive elements, problem-solution pairs","Primary brand, bright secondary colors for feature cards, contrasting accent for CTAs","Supporting colors for: benefits (green), problems (red/orange), features (blue/purple), social proof (neutral)","Card hover effects (lift/scale), icon animations on scroll, feature toggle animations, smooth section transitions","Enterprise SaaS, software tools landing pages, platform services, complex product explanations, B2B products","Simple product pages, early-stage startups with few features, entertainment landing pages",✓ Full,✓ Full,⚡ Good,✓ WCAG AA,✓ Good,✓ High
23,Minimal & Direct,Landing Page,"Minimal text, white space heavy, single column layout, direct messaging, clean typography, visual-centric, fast-loading","Monochromatic primary, white background, single accent color for CTA, black/dark grey text","Minimal secondary colors, reserved for critical CTAs only, neutral supporting elements","Very subtle hover effects, minimal animations, fast page load (no heavy animations), smooth scroll","Simple service landing pages, indie products, consulting services, micro SaaS, freelancer portfolios","Feature-heavy products, complex explanations, multi-product showcases",✓ Full,✓ Full,⚡ Excellent,✓ WCAG AAA,✓ Full,✓ High 23,Minimal & Direct,Landing Page,"Minimal text, white space heavy, single column layout, direct messaging, clean typography, visual-centric, fast-loading","Monochromatic primary, white background, single accent color for CTA, black/dark grey text","Minimal secondary colors, reserved for critical CTAs only, neutral supporting elements","Very subtle hover effects, minimal animations, fast page load (no heavy animations), smooth scroll","Simple service landing pages, indie products, consulting services, micro SaaS, freelancer portfolios","Feature-heavy products, complex explanations, multi-product showcases",✓ Full,✓ Full,⚡ Excellent,✓ WCAG AAA,✓ Full,✓ High
24,Social Proof-Focused,Landing Page,"Testimonials prominent, client logos displayed, case studies sections, reviews/ratings, user avatars, success metrics, credibility markers","Primary brand, trust colors (blue), success/growth colors (green), neutral backgrounds","Testimonial highlight colors, logo grid backgrounds (light grey), badge/achievement colors","Testimonial carousel animations, logo grid fade-in, stat counter animations (number count-up), review star ratings","B2B SaaS, professional services, premium products, e-commerce conversion pages, established brands","Startup MVPs, products without users, niche/experimental products",✓ Full,✓ Full,⚡ Good,✓ WCAG AA,✓ Full,✓ High 24,Social Proof-Focused,Landing Page,"Testimonials prominent, client logos displayed, case studies sections, reviews/ratings, user avatars, success metrics, credibility markers","Primary brand, trust colors (blue), success/growth colors (green), neutral backgrounds","Testimonial highlight colors, logo grid backgrounds (light grey), badge/achievement colors","Testimonial carousel animations, logo grid fade-in, stat counter animations (number count-up), review star ratings","B2B SaaS, professional services, premium products, e-commerce conversion pages, established brands","Startup MVPs, products without users, niche/experimental products",✓ Full,✓ Full,⚡ Good,✓ WCAG AA,✓ Full,✓ High
25,Interactive Product Demo,Landing Page,"Embedded product mockup/video, interactive elements, product walkthrough, step-by-step guides, hover-to-reveal features, embedded demos","Primary brand, interface colors matching product, demo highlight colors for interactive elements","Product UI colors, tutorial step colors (numbered progression), hover state indicators","Product animation playback, step progression animations, hover reveal effects, smooth zoom on interaction","SaaS platforms, tool/software products, productivity apps landing pages, developer tools, productivity software","Simple services, consulting, non-digital products, complexity-averse audiences",✓ Full,✓ Full,⚠ Good (video/interactive),✓ WCAG AA,✓ Good,✓ Very High 25,Interactive Product Demo,Landing Page,"Embedded product mockup/video, interactive elements, product walkthrough, step-by-step guides, hover-to-reveal features, embedded demos","Primary brand, interface colors matching product, demo highlight colors for interactive elements","Product UI colors, tutorial step colors (numbered progression), hover state indicators","Product animation playback, step progression animations, hover reveal effects, smooth zoom on interaction","SaaS platforms, tool/software products, productivity apps landing pages, developer tools, productivity software","Simple services, consulting, non-digital products, complexity-averse audiences",✓ Full,✓ Full,⚠ Good (video/interactive),✓ WCAG AA,✓ Good,✓ Very High
26,Trust & Authority,Landing Page,"Certificates/badges displayed, expert credentials, case studies with metrics, before/after comparisons, industry recognition, security badges","Professional colors (blue/grey), trust colors, certification badge colors (gold/silver accents)","Certificate highlight colors, metric showcase colors, comparison highlight (success green)","Badge hover effects, metric pulse animations, certificate carousel, smooth stat reveal","Healthcare/medical landing pages, financial services, enterprise software, premium/luxury products, legal services","Casual products, entertainment, viral/social-first products",✓ Full,✓ Full,⚡ Excellent,✓ WCAG AAA,✓ Full,✓ High 26,Trust & Authority,Landing Page,"Certificates/badges displayed, expert credentials, case studies with metrics, before/after comparisons, industry recognition, security badges","Professional colors (blue/grey), trust colors, certification badge colors (gold/silver accents)","Certificate highlight colors, metric showcase colors, comparison highlight (success green)","Badge hover effects, metric pulse animations, certificate carousel, smooth stat reveal","Healthcare/medical landing pages, financial services, enterprise software, premium/luxury products, legal services","Casual products, entertainment, viral/social-first products",✓ Full,✓ Full,⚡ Excellent,✓ WCAG AAA,✓ Full,✓ High
27,Storytelling-Driven,Landing Page,"Narrative flow, visual story progression, section transitions, consistent character/brand voice, emotional messaging, journey visualization","Brand primary, warm/emotional colors, varied accent colors per story section, high visual variety","Story section color coding, emotional state colors (calm, excitement, success), transitional gradients","Section-to-section animations, scroll-triggered reveals, character/icon animations, morphing transitions, parallax narrative","Brand/startup stories, mission-driven products, premium/lifestyle brands, documentary-style products, educational","Technical/complex products (unless narrative-driven), traditional enterprise software",✓ Full,✓ Full,⚠ Moderate (animations),✓ WCAG AA,✓ Good,✓ High 27,Storytelling-Driven,Landing Page,"Narrative flow, visual story progression, section transitions, consistent character/brand voice, emotional messaging, journey visualization","Brand primary, warm/emotional colors, varied accent colors per story section, high visual variety","Story section color coding, emotional state colors (calm, excitement, success), transitional gradients","Section-to-section animations, scroll-triggered reveals, character/icon animations, morphing transitions, parallax narrative","Brand/startup stories, mission-driven products, premium/lifestyle brands, documentary-style products, educational","Technical/complex products (unless narrative-driven), traditional enterprise software",✓ Full,✓ Full,⚠ Moderate (animations),✓ WCAG AA,✓ Good,✓ High
28,Data-Dense Dashboard,BI/Analytics,"Multiple charts/widgets, data tables, KPI cards, minimal padding, grid layout, space-efficient, maximum data visibility","Neutral primary (light grey/white #F5F5F5), data colors (blue/green/red), dark text #333333","Chart colors: success (green #22C55E), warning (amber #F59E0B), alert (red #EF4444), neutral (grey)","Hover tooltips, chart zoom on click, row highlighting on hover, smooth filter animations, data loading spinners","Business intelligence dashboards, financial analytics, enterprise reporting, operational dashboards, data warehousing","Marketing dashboards, consumer-facing analytics, simple reporting",✓ Full,✓ Full,⚡ Excellent,✓ WCAG AA,◐ Medium,✗ Not applicable 28,Data-Dense Dashboard,BI/Analytics,"Multiple charts/widgets, data tables, KPI cards, minimal padding, grid layout, space-efficient, maximum data visibility","Neutral primary (light grey/white #F5F5F5), data colors (blue/green/red), dark text #333333","Chart colors: success (green #22C55E), warning (amber #F59E0B), alert (red #EF4444), neutral (grey)","Hover tooltips, chart zoom on click, row highlighting on hover, smooth filter animations, data loading spinners","Business intelligence dashboards, financial analytics, enterprise reporting, operational dashboards, data warehousing","Marketing dashboards, consumer-facing analytics, simple reporting",✓ Full,✓ Full,⚡ Excellent,✓ WCAG AA,◐ Medium,✗ Not applicable
29,Heat Map & Heatmap Style,BI/Analytics,"Color-coded grid/matrix, data intensity visualization, geographical heat maps, correlation matrices, cell-based representation, gradient coloring","Gradient scale: Cool (blue #0080FF) to hot (red #FF0000), neutral middle (white/yellow)","Support gradients: Light (cool blue) to dark (warm red), divergent for positive/negative data, monochromatic options","Color gradient transitions on data change, cell highlighting on hover, tooltip reveal on click, smooth color animation","Geographical analysis, performance matrices, correlation analysis, user behavior heatmaps, temperature/intensity data","Linear data representation, categorical comparisons (use bar charts), small datasets",✓ Full,✓ Full (with adjustments),⚡ Excellent,⚠ Colorblind considerations,◐ Medium,✗ Not applicable 29,Heat Map & Heatmap Style,BI/Analytics,"Color-coded grid/matrix, data intensity visualization, geographical heat maps, correlation matrices, cell-based representation, gradient coloring","Gradient scale: Cool (blue #0080FF) to hot (red #FF0000), neutral middle (white/yellow)","Support gradients: Light (cool blue) to dark (warm red), divergent for positive/negative data, monochromatic options","Color gradient transitions on data change, cell highlighting on hover, tooltip reveal on click, smooth color animation","Geographical analysis, performance matrices, correlation analysis, user behavior heatmaps, temperature/intensity data","Linear data representation, categorical comparisons (use bar charts), small datasets",✓ Full,✓ Full (with adjustments),⚡ Excellent,⚠ Colorblind considerations,◐ Medium,✗ Not applicable
30,Executive Dashboard,BI/Analytics,"High-level KPIs, large key metrics, minimal detail, summary view, trend indicators, at-a-glance insights, executive summary","Brand colors, professional palette (blue/grey/white), accent for KPIs, red for alerts/concerns","KPI highlight colors: positive (green), negative (red), neutral (grey), trend arrow colors","KPI value animations (count-up), trend arrow direction animations, metric card hover lift, alert pulse effect","C-suite dashboards, business summary reports, decision-maker dashboards, strategic planning views","Detailed analyst dashboards, technical deep-dives, operational monitoring",✓ Full,✓ Full,⚡ Excellent,✓ WCAG AA,✗ Low (not mobile-optimized),✗ Not applicable 30,Executive Dashboard,BI/Analytics,"High-level KPIs, large key metrics, minimal detail, summary view, trend indicators, at-a-glance insights, executive summary","Brand colors, professional palette (blue/grey/white), accent for KPIs, red for alerts/concerns","KPI highlight colors: positive (green), negative (red), neutral (grey), trend arrow colors","KPI value animations (count-up), trend arrow direction animations, metric card hover lift, alert pulse effect","C-suite dashboards, business summary reports, decision-maker dashboards, strategic planning views","Detailed analyst dashboards, technical deep-dives, operational monitoring",✓ Full,✓ Full,⚡ Excellent,✓ WCAG AA,✗ Low (not mobile-optimized),✗ Not applicable
31,Real-Time Monitoring,BI/Analytics,"Live data updates, status indicators, alert notifications, streaming data visualization, active monitoring, streaming charts","Alert colors: critical (red #FF0000), warning (orange #FFA500), normal (green #22C55E), updating (blue animation)","Status indicator colors, chart line colors varying by metric, streaming data highlight colors","Real-time chart animations, alert pulse/glow, status indicator blink animation, smooth data stream updates, loading effect","System monitoring dashboards, DevOps dashboards, real-time analytics, stock market dashboards, live event tracking","Historical analysis, long-term trend reports, archived data dashboards",✓ Full,✓ Full,⚡ Good (real-time load),✓ WCAG AA,◐ Medium,✗ Not applicable 31,Real-Time Monitoring,BI/Analytics,"Live data updates, status indicators, alert notifications, streaming data visualization, active monitoring, streaming charts","Alert colors: critical (red #FF0000), warning (orange #FFA500), normal (green #22C55E), updating (blue animation)","Status indicator colors, chart line colors varying by metric, streaming data highlight colors","Real-time chart animations, alert pulse/glow, status indicator blink animation, smooth data stream updates, loading effect","System monitoring dashboards, DevOps dashboards, real-time analytics, stock market dashboards, live event tracking","Historical analysis, long-term trend reports, archived data dashboards",✓ Full,✓ Full,⚡ Good (real-time load),✓ WCAG AA,◐ Medium,✗ Not applicable
32,Drill-Down Analytics,BI/Analytics,"Hierarchical data exploration, expandable sections, interactive drill-down paths, summary-to-detail flow, context preservation","Primary brand, breadcrumb colors, drill-level indicator colors, hierarchy depth colors","Drill-down path indicator colors, level-specific colors, highlight colors for selected level, transition colors","Drill-down expand animations, breadcrumb click transitions, smooth detail reveal, level change smooth, data reload animation","Sales analytics, product analytics, funnel analysis, multi-dimensional data exploration, business intelligence","Simple linear data, single-metric dashboards, streaming real-time dashboards",✓ Full,✓ Full,⚡ Good,✓ WCAG AA,◐ Medium,✗ Not applicable 32,Drill-Down Analytics,BI/Analytics,"Hierarchical data exploration, expandable sections, interactive drill-down paths, summary-to-detail flow, context preservation","Primary brand, breadcrumb colors, drill-level indicator colors, hierarchy depth colors","Drill-down path indicator colors, level-specific colors, highlight colors for selected level, transition colors","Drill-down expand animations, breadcrumb click transitions, smooth detail reveal, level change smooth, data reload animation","Sales analytics, product analytics, funnel analysis, multi-dimensional data exploration, business intelligence","Simple linear data, single-metric dashboards, streaming real-time dashboards",✓ Full,✓ Full,⚡ Good,✓ WCAG AA,◐ Medium,✗ Not applicable
33,Comparative Analysis Dashboard,BI/Analytics,"Side-by-side comparisons, period-over-period metrics, A/B test results, regional comparisons, performance benchmarks","Comparison colors: primary (blue), comparison (orange/purple), delta indicator (green/red)","Winning metric color (green), losing metric color (red), neutral comparison (grey), benchmark colors","Comparison bar animations (grow to value), delta indicator animations (direction arrows), highlight on compare","Period-over-period reporting, A/B test dashboards, market comparison, competitive analysis, regional performance","Single metric dashboards, future projections (use forecasting), real-time only (no historical)",✓ Full,✓ Full,⚡ Excellent,✓ WCAG AA,◐ Medium,✗ Not applicable 33,Comparative Analysis Dashboard,BI/Analytics,"Side-by-side comparisons, period-over-period metrics, A/B test results, regional comparisons, performance benchmarks","Comparison colors: primary (blue), comparison (orange/purple), delta indicator (green/red)","Winning metric color (green), losing metric color (red), neutral comparison (grey), benchmark colors","Comparison bar animations (grow to value), delta indicator animations (direction arrows), highlight on compare","Period-over-period reporting, A/B test dashboards, market comparison, competitive analysis, regional performance","Single metric dashboards, future projections (use forecasting), real-time only (no historical)",✓ Full,✓ Full,⚡ Excellent,✓ WCAG AA,◐ Medium,✗ Not applicable
34,Predictive Analytics,BI/Analytics,"Forecast lines, confidence intervals, trend projections, scenario modeling, AI-driven insights, anomaly detection visualization","Forecast line color (distinct from actual), confidence interval shading, anomaly highlight (red alert), trend colors","High confidence (dark color), low confidence (light color), anomaly colors (red/orange), normal trend (green/blue)","Forecast line animation on draw, confidence band fade-in, anomaly pulse alert, smoothing function animations","Forecasting dashboards, anomaly detection systems, trend prediction dashboards, AI-powered analytics, budget planning","Historical-only dashboards, simple reporting, real-time operational dashboards",✓ Full,✓ Full,⚠ Good (computation),✓ WCAG AA,◐ Medium,✗ Not applicable 34,Predictive Analytics,BI/Analytics,"Forecast lines, confidence intervals, trend projections, scenario modeling, AI-driven insights, anomaly detection visualization","Forecast line color (distinct from actual), confidence interval shading, anomaly highlight (red alert), trend colors","High confidence (dark color), low confidence (light color), anomaly colors (red/orange), normal trend (green/blue)","Forecast line animation on draw, confidence band fade-in, anomaly pulse alert, smoothing function animations","Forecasting dashboards, anomaly detection systems, trend prediction dashboards, AI-powered analytics, budget planning","Historical-only dashboards, simple reporting, real-time operational dashboards",✓ Full,✓ Full,⚠ Good (computation),✓ WCAG AA,◐ Medium,✗ Not applicable
35,User Behavior Analytics,BI/Analytics,"Funnel visualization, user flow diagrams, conversion tracking, engagement metrics, user journey mapping, cohort analysis","Funnel stage colors: high engagement (green), drop-off (red), conversion (blue), user flow arrows (grey)","Stage completion colors (success), abandonment colors (warning), engagement levels (gradient), cohort colors","Funnel animation (fill-down), flow diagram animations (connection draw), conversion pulse, engagement bar fill","Conversion funnel analysis, user journey tracking, engagement analytics, cohort analysis, retention tracking","Real-time operational metrics, technical system monitoring, financial transactions",✓ Full,✓ Full,⚡ Good,✓ WCAG AA,✓ Good,✗ Not applicable 35,User Behavior Analytics,BI/Analytics,"Funnel visualization, user flow diagrams, conversion tracking, engagement metrics, user journey mapping, cohort analysis","Funnel stage colors: high engagement (green), drop-off (red), conversion (blue), user flow arrows (grey)","Stage completion colors (success), abandonment colors (warning), engagement levels (gradient), cohort colors","Funnel animation (fill-down), flow diagram animations (connection draw), conversion pulse, engagement bar fill","Conversion funnel analysis, user journey tracking, engagement analytics, cohort analysis, retention tracking","Real-time operational metrics, technical system monitoring, financial transactions",✓ Full,✓ Full,⚡ Good,✓ WCAG AA,✓ Good,✗ Not applicable
36,Financial Dashboard,BI/Analytics,"Revenue metrics, profit/loss visualization, budget tracking, financial ratios, portfolio performance, cash flow, audit trail","Financial colors: profit (green #22C55E), loss (red #EF4444), neutral (grey), trust (dark blue #003366)","Revenue highlight (green), expenses (red), budget variance (orange/red), balance (grey), accuracy (blue)","Number animations (count-up), trend direction indicators, percentage change animations, profit/loss color transitions","Financial reporting, accounting dashboards, portfolio tracking, budget monitoring, banking analytics","Simple business dashboards, entertainment/social metrics, non-financial data",✓ Full,✓ Full,⚡ Excellent,✓ WCAG AAA,✗ Low,✗ Not applicable 36,Financial Dashboard,BI/Analytics,"Revenue metrics, profit/loss visualization, budget tracking, financial ratios, portfolio performance, cash flow, audit trail","Financial colors: profit (green #22C55E), loss (red #EF4444), neutral (grey), trust (dark blue #003366)","Revenue highlight (green), expenses (red), budget variance (orange/red), balance (grey), accuracy (blue)","Number animations (count-up), trend direction indicators, percentage change animations, profit/loss color transitions","Financial reporting, accounting dashboards, portfolio tracking, budget monitoring, banking analytics","Simple business dashboards, entertainment/social metrics, non-financial data",✓ Full,✓ Full,⚡ Excellent,✓ WCAG AAA,✗ Low,✗ Not applicable
37,Sales Intelligence Dashboard,BI/Analytics,"Deal pipeline, sales metrics, territory performance, sales rep leaderboard, win-loss analysis, quota tracking, forecast accuracy","Sales colors: won (green), lost (red), in-progress (blue), blocked (orange), quota met (gold), quota missed (grey)","Pipeline stage colors, rep performance colors, quota achievement colors, forecast accuracy colors","Deal movement animations, metric updates, leaderboard ranking changes, gauge needle movements, status change highlights","CRM dashboards, sales management, opportunity tracking, performance management, quota planning","Marketing analytics, customer support metrics, HR dashboards",✓ Full,✓ Full,⚡ Good,✓ WCAG AA,◐ Medium,✗ Not applicable,"Recharts 9/10, Chart.js 9/10",2020s Modern,Medium 37,Sales Intelligence Dashboard,BI/Analytics,"Deal pipeline, sales metrics, territory performance, sales rep leaderboard, win-loss analysis, quota tracking, forecast accuracy","Sales colors: won (green), lost (red), in-progress (blue), blocked (orange), quota met (gold), quota missed (grey)","Pipeline stage colors, rep performance colors, quota achievement colors, forecast accuracy colors","Deal movement animations, metric updates, leaderboard ranking changes, gauge needle movements, status change highlights","CRM dashboards, sales management, opportunity tracking, performance management, quota planning","Marketing analytics, customer support metrics, HR dashboards",✓ Full,✓ Full,⚡ Good,✓ WCAG AA,◐ Medium,✗ Not applicable,"Recharts 9/10, Chart.js 9/10",2020s Modern,Medium
38,Neubrutalism,General,"Bold borders, black outlines, primary colors, thick shadows, no gradients, flat colors, 45° shadows, playful, Gen Z","#FFEB3B (Yellow), #FF5252 (Red), #2196F3 (Blue), #000000 (Black borders)","Limited accent colors, high contrast combinations, no gradients allowed","box-shadow: 4px 4px 0 #000, border: 3px solid #000, no gradients, sharp corners (0px), bold typography","Gen Z brands, startups, creative agencies, Figma-style apps, Notion-style interfaces, tech blogs","Luxury brands, finance, healthcare, conservative industries (too playful)",✓ Full,✓ Full,⚡ Excellent,✓ WCAG AAA,✓ High,✓ High,"Tailwind 10/10, Bootstrap 8/10",2020s Modern,Low 38,Neubrutalism,General,"Bold borders, black outlines, primary colors, thick shadows, no gradients, flat colors, 45° shadows, playful, Gen Z","#FFEB3B (Yellow), #FF5252 (Red), #2196F3 (Blue), #000000 (Black borders)","Limited accent colors, high contrast combinations, no gradients allowed","box-shadow: 4px 4px 0 #000, border: 3px solid #000, no gradients, sharp corners (0px), bold typography","Gen Z brands, startups, creative agencies, Figma-style apps, Notion-style interfaces, tech blogs","Luxury brands, finance, healthcare, conservative industries (too playful)",✓ Full,✓ Full,⚡ Excellent,✓ WCAG AAA,✓ High,✓ High,"Tailwind 10/10, Bootstrap 8/10",2020s Modern,Low
39,Bento Box Grid,General,"Modular cards, asymmetric grid, varied sizes, Apple-style, dashboard tiles, negative space, clean hierarchy, cards","Neutral base + brand accent, #FFFFFF, #F5F5F5, brand primary","Subtle gradients, shadow variations, accent highlights for interactive cards","grid-template with varied spans, rounded-xl (16px), subtle shadows, hover scale (1.02), smooth transitions","Dashboards, product pages, portfolios, Apple-style marketing, feature showcases, SaaS","Dense data tables, text-heavy content, real-time monitoring",✓ Full,✓ Full,⚡ Excellent,✓ WCAG AA,✓ High,✓ High,"Tailwind 10/10, CSS Grid 10/10",2020s Apple,Low 39,Bento Box Grid,General,"Modular cards, asymmetric grid, varied sizes, Apple-style, dashboard tiles, negative space, clean hierarchy, cards","Neutral base + brand accent, #FFFFFF, #F5F5F5, brand primary","Subtle gradients, shadow variations, accent highlights for interactive cards","grid-template with varied spans, rounded-xl (16px), subtle shadows, hover scale (1.02), smooth transitions","Dashboards, product pages, portfolios, Apple-style marketing, feature showcases, SaaS","Dense data tables, text-heavy content, real-time monitoring",✓ Full,✓ Full,⚡ Excellent,✓ WCAG AA,✓ High,✓ High,"Tailwind 10/10, CSS Grid 10/10",2020s Apple,Low
40,Y2K Aesthetic,General,"Neon pink, chrome, metallic, bubblegum, iridescent, glossy, retro-futurism, 2000s, futuristic nostalgia","#FF69B4 (Hot Pink), #00FFFF (Cyan), #C0C0C0 (Silver), #9400D3 (Purple)","Metallic gradients, glossy overlays, iridescent effects, chrome textures","linear-gradient metallic, glossy buttons, 3D chrome effects, glow animations, bubble shapes","Fashion brands, music platforms, Gen Z brands, nostalgia marketing, entertainment, youth-focused","B2B enterprise, healthcare, finance, conservative industries, elderly users",✓ Full,◐ Partial,⚠ Good,⚠ Check contrast,✓ Good,✓ High,"Tailwind 8/10, CSS-in-JS 9/10",Y2K 2000s,Medium 40,Y2K Aesthetic,General,"Neon pink, chrome, metallic, bubblegum, iridescent, glossy, retro-futurism, 2000s, futuristic nostalgia","#FF69B4 (Hot Pink), #00FFFF (Cyan), #C0C0C0 (Silver), #9400D3 (Purple)","Metallic gradients, glossy overlays, iridescent effects, chrome textures","linear-gradient metallic, glossy buttons, 3D chrome effects, glow animations, bubble shapes","Fashion brands, music platforms, Gen Z brands, nostalgia marketing, entertainment, youth-focused","B2B enterprise, healthcare, finance, conservative industries, elderly users",✓ Full,◐ Partial,⚠ Good,⚠ Check contrast,✓ Good,✓ High,"Tailwind 8/10, CSS-in-JS 9/10",Y2K 2000s,Medium
41,Cyberpunk UI,General,"Neon, dark mode, terminal, HUD, sci-fi, glitch, dystopian, futuristic, matrix, tech noir","#00FF00 (Matrix Green), #FF00FF (Magenta), #00FFFF (Cyan), #0D0D0D (Dark)","Neon gradients, scanline overlays, glitch colors, terminal green accents","Neon glow (text-shadow), glitch animations (skew/offset), scanlines (::before overlay), terminal fonts","Gaming platforms, tech products, crypto apps, sci-fi applications, developer tools, entertainment","Corporate enterprise, healthcare, family apps, conservative brands, elderly users",✗ No,✓ Only,⚠ Moderate,⚠ Limited (dark+neon),◐ Medium,◐ Medium,"Tailwind 8/10, Custom CSS 10/10",2020s Cyberpunk,Medium 41,Cyberpunk UI,General,"Neon, dark mode, terminal, HUD, sci-fi, glitch, dystopian, futuristic, matrix, tech noir","#00FF00 (Matrix Green), #FF00FF (Magenta), #00FFFF (Cyan), #0D0D0D (Dark)","Neon gradients, scanline overlays, glitch colors, terminal green accents","Neon glow (text-shadow), glitch animations (skew/offset), scanlines (::before overlay), terminal fonts","Gaming platforms, tech products, crypto apps, sci-fi applications, developer tools, entertainment","Corporate enterprise, healthcare, family apps, conservative brands, elderly users",✗ No,✓ Only,⚠ Moderate,⚠ Limited (dark+neon),◐ Medium,◐ Medium,"Tailwind 8/10, Custom CSS 10/10",2020s Cyberpunk,Medium
42,Organic Biophilic,General,"Nature, organic shapes, green, sustainable, rounded, flowing, wellness, earthy, natural textures","#228B22 (Forest Green), #8B4513 (Earth Brown), #87CEEB (Sky Blue), #F5F5DC (Beige)","Natural gradients, earth tones, sky blues, organic textures, wood/stone colors","Rounded corners (16-24px), organic curves (border-radius variations), natural shadows, flowing SVG shapes","Wellness apps, sustainability brands, eco products, health apps, meditation, organic food brands","Tech-focused products, gaming, industrial, urban brands",✓ Full,✓ Full,⚡ Excellent,✓ WCAG AA,✓ High,✓ High,"Tailwind 10/10, CSS 10/10",2020s Sustainable,Low 42,Organic Biophilic,General,"Nature, organic shapes, green, sustainable, rounded, flowing, wellness, earthy, natural textures","#228B22 (Forest Green), #8B4513 (Earth Brown), #87CEEB (Sky Blue), #F5F5DC (Beige)","Natural gradients, earth tones, sky blues, organic textures, wood/stone colors","Rounded corners (16-24px), organic curves (border-radius variations), natural shadows, flowing SVG shapes","Wellness apps, sustainability brands, eco products, health apps, meditation, organic food brands","Tech-focused products, gaming, industrial, urban brands",✓ Full,✓ Full,⚡ Excellent,✓ WCAG AA,✓ High,✓ High,"Tailwind 10/10, CSS 10/10",2020s Sustainable,Low
43,AI-Native UI,General,"Chatbot, conversational, voice, assistant, agentic, ambient, minimal chrome, streaming text, AI interactions","Neutral + single accent, #6366F1 (AI Purple), #10B981 (Success), #F5F5F5 (Background)","Status indicators, streaming highlights, context card colors, subtle accent variations","Typing indicators (3-dot pulse), streaming text animations, pulse animations, context cards, smooth reveals","AI products, chatbots, voice assistants, copilots, AI-powered tools, conversational interfaces","Traditional forms, data-heavy dashboards, print-first content",✓ Full,✓ Full,⚡ Excellent,✓ WCAG AA,✓ High,✓ High,"Tailwind 10/10, React 10/10",2020s AI-Era,Low 43,AI-Native UI,General,"Chatbot, conversational, voice, assistant, agentic, ambient, minimal chrome, streaming text, AI interactions","Neutral + single accent, #6366F1 (AI Purple), #10B981 (Success), #F5F5F5 (Background)","Status indicators, streaming highlights, context card colors, subtle accent variations","Typing indicators (3-dot pulse), streaming text animations, pulse animations, context cards, smooth reveals","AI products, chatbots, voice assistants, copilots, AI-powered tools, conversational interfaces","Traditional forms, data-heavy dashboards, print-first content",✓ Full,✓ Full,⚡ Excellent,✓ WCAG AA,✓ High,✓ High,"Tailwind 10/10, React 10/10",2020s AI-Era,Low
44,Memphis Design,General,"80s, geometric, playful, postmodern, shapes, patterns, squiggles, triangles, neon, abstract, bold","#FF71CE (Hot Pink), #FFCE5C (Yellow), #86CCCA (Teal), #6A7BB4 (Blue Purple)","Complementary geometric colors, pattern fills, contrasting accent shapes","transform: rotate(), clip-path: polygon(), mix-blend-mode, repeating patterns, bold shapes","Creative agencies, music sites, youth brands, event promotion, artistic portfolios, entertainment","Corporate finance, healthcare, legal, elderly users, conservative brands",✓ Full,✓ Full,⚡ Excellent,⚠ Check contrast,✓ Good,◐ Medium,"Tailwind 9/10, CSS 10/10",1980s Postmodern,Medium 44,Memphis Design,General,"80s, geometric, playful, postmodern, shapes, patterns, squiggles, triangles, neon, abstract, bold","#FF71CE (Hot Pink), #FFCE5C (Yellow), #86CCCA (Teal), #6A7BB4 (Blue Purple)","Complementary geometric colors, pattern fills, contrasting accent shapes","transform: rotate(), clip-path: polygon(), mix-blend-mode, repeating patterns, bold shapes","Creative agencies, music sites, youth brands, event promotion, artistic portfolios, entertainment","Corporate finance, healthcare, legal, elderly users, conservative brands",✓ Full,✓ Full,⚡ Excellent,⚠ Check contrast,✓ Good,◐ Medium,"Tailwind 9/10, CSS 10/10",1980s Postmodern,Medium
45,Vaporwave,General,"Synthwave, retro-futuristic, 80s-90s, neon, glitch, nostalgic, sunset gradient, dreamy, aesthetic","#FF71CE (Pink), #01CDFE (Cyan), #05FFA1 (Mint), #B967FF (Purple)","Sunset gradients, glitch overlays, VHS effects, neon accents, pastel variations","text-shadow glow, linear-gradient, filter: hue-rotate(), glitch animations, retro scan lines","Music platforms, gaming, creative portfolios, tech startups, entertainment, artistic projects","Business apps, e-commerce, education, healthcare, enterprise software",✓ Full,✓ Dark focused,⚠ Moderate,⚠ Poor (motion),◐ Medium,◐ Medium,"Tailwind 8/10, CSS-in-JS 9/10",1980s-90s Retro,Medium 45,Vaporwave,General,"Synthwave, retro-futuristic, 80s-90s, neon, glitch, nostalgic, sunset gradient, dreamy, aesthetic","#FF71CE (Pink), #01CDFE (Cyan), #05FFA1 (Mint), #B967FF (Purple)","Sunset gradients, glitch overlays, VHS effects, neon accents, pastel variations","text-shadow glow, linear-gradient, filter: hue-rotate(), glitch animations, retro scan lines","Music platforms, gaming, creative portfolios, tech startups, entertainment, artistic projects","Business apps, e-commerce, education, healthcare, enterprise software",✓ Full,✓ Dark focused,⚠ Moderate,⚠ Poor (motion),◐ Medium,◐ Medium,"Tailwind 8/10, CSS-in-JS 9/10",1980s-90s Retro,Medium
46,Dimensional Layering,General,"Depth, overlapping, z-index, layers, 3D, shadows, elevation, floating, cards, spatial hierarchy","Neutral base (#FFFFFF, #F5F5F5, #E0E0E0) + brand accent for elevated elements","Shadow variations (sm/md/lg/xl), elevation colors, highlight colors for top layers","z-index stacking, box-shadow elevation (4 levels), transform: translateZ(), backdrop-filter, parallax","Dashboards, card layouts, modals, navigation, product showcases, SaaS interfaces","Print-style layouts, simple blogs, low-end devices, flat design requirements",✓ Full,✓ Full,⚠ Good,⚠ Moderate (SR issues),✓ Good,✓ High,"Tailwind 10/10, MUI 10/10, Chakra 10/10",2020s Modern,Medium 46,Dimensional Layering,General,"Depth, overlapping, z-index, layers, 3D, shadows, elevation, floating, cards, spatial hierarchy","Neutral base (#FFFFFF, #F5F5F5, #E0E0E0) + brand accent for elevated elements","Shadow variations (sm/md/lg/xl), elevation colors, highlight colors for top layers","z-index stacking, box-shadow elevation (4 levels), transform: translateZ(), backdrop-filter, parallax","Dashboards, card layouts, modals, navigation, product showcases, SaaS interfaces","Print-style layouts, simple blogs, low-end devices, flat design requirements",✓ Full,✓ Full,⚠ Good,⚠ Moderate (SR issues),✓ Good,✓ High,"Tailwind 10/10, MUI 10/10, Chakra 10/10",2020s Modern,Medium
47,Exaggerated Minimalism,General,"Bold minimalism, oversized typography, high contrast, negative space, loud minimal, statement design","#000000 (Black), #FFFFFF (White), single vibrant accent only","Minimal - single accent color, no secondary colors, extreme restraint","font-size: clamp(3rem 10vw 12rem), font-weight: 900, letter-spacing: -0.05em, massive whitespace","Fashion, architecture, portfolios, agency landing pages, luxury brands, editorial","E-commerce catalogs, dashboards, forms, data-heavy, elderly users, complex apps",✓ Full,✓ Full,⚡ Excellent,✓ WCAG AA,✓ High,✓ High,"Tailwind 10/10, Typography.js 10/10",2020s Modern,Low 47,Exaggerated Minimalism,General,"Bold minimalism, oversized typography, high contrast, negative space, loud minimal, statement design","#000000 (Black), #FFFFFF (White), single vibrant accent only","Minimal - single accent color, no secondary colors, extreme restraint","font-size: clamp(3rem 10vw 12rem), font-weight: 900, letter-spacing: -0.05em, massive whitespace","Fashion, architecture, portfolios, agency landing pages, luxury brands, editorial","E-commerce catalogs, dashboards, forms, data-heavy, elderly users, complex apps",✓ Full,✓ Full,⚡ Excellent,✓ WCAG AA,✓ High,✓ High,"Tailwind 10/10, Typography.js 10/10",2020s Modern,Low
48,Kinetic Typography,General,"Motion text, animated type, moving letters, dynamic, typing effect, morphing, scroll-triggered text","Flexible - high contrast recommended, bold colors for emphasis, animation-friendly palette","Accent colors for emphasis, transition colors, gradient text fills","@keyframes text animation, typing effect, background-clip: text, GSAP ScrollTrigger, split text","Hero sections, marketing sites, video platforms, storytelling, creative portfolios, landing pages","Long-form content, accessibility-critical, data interfaces, forms, elderly users",✓ Full,✓ Full,⚠ Moderate,❌ Poor (motion),✓ Good,✓ Very High,"GSAP 10/10, Framer Motion 10/10",2020s Modern,High 48,Kinetic Typography,General,"Motion text, animated type, moving letters, dynamic, typing effect, morphing, scroll-triggered text","Flexible - high contrast recommended, bold colors for emphasis, animation-friendly palette","Accent colors for emphasis, transition colors, gradient text fills","@keyframes text animation, typing effect, background-clip: text, GSAP ScrollTrigger, split text","Hero sections, marketing sites, video platforms, storytelling, creative portfolios, landing pages","Long-form content, accessibility-critical, data interfaces, forms, elderly users",✓ Full,✓ Full,⚠ Moderate,❌ Poor (motion),✓ Good,✓ Very High,"GSAP 10/10, Framer Motion 10/10",2020s Modern,High
49,Parallax Storytelling,General,"Scroll-driven, narrative, layered scrolling, immersive, progressive disclosure, cinematic, scroll-triggered","Story-dependent, often gradients and natural colors, section-specific palettes","Section transition colors, depth layer colors, narrative mood colors","transform: translateY(scroll), position: fixed/sticky, perspective: 1px, scroll-triggered animations","Brand storytelling, product launches, case studies, portfolios, annual reports, marketing campaigns","E-commerce, dashboards, mobile-first, SEO-critical, accessibility-required",✓ Full,✓ Full,❌ Poor,❌ Poor (motion),✗ Low,✓ High,"GSAP ScrollTrigger 10/10, Locomotive Scroll 10/10",2020s Modern,High 49,Parallax Storytelling,General,"Scroll-driven, narrative, layered scrolling, immersive, progressive disclosure, cinematic, scroll-triggered","Story-dependent, often gradients and natural colors, section-specific palettes","Section transition colors, depth layer colors, narrative mood colors","transform: translateY(scroll), position: fixed/sticky, perspective: 1px, scroll-triggered animations","Brand storytelling, product launches, case studies, portfolios, annual reports, marketing campaigns","E-commerce, dashboards, mobile-first, SEO-critical, accessibility-required",✓ Full,✓ Full,❌ Poor,❌ Poor (motion),✗ Low,✓ High,"GSAP ScrollTrigger 10/10, Locomotive Scroll 10/10",2020s Modern,High
50,Swiss Modernism 2.0,General,"Grid system, Helvetica, modular, asymmetric, international style, rational, clean, mathematical spacing","#000000, #FFFFFF, #F5F5F5, single vibrant accent only","Minimal secondary, accent for emphasis only, no gradients","display: grid, grid-template-columns: repeat(12 1fr), gap: 1rem, mathematical ratios, clear hierarchy","Corporate sites, architecture, editorial, SaaS, museums, professional services, documentation","Playful brands, children's sites, entertainment, gaming, emotional storytelling",✓ Full,✓ Full,⚡ Excellent,✓ WCAG AAA,✓ High,✓ High,"Tailwind 10/10, Bootstrap 9/10, Foundation 10/10",1950s Swiss + 2020s,Low 50,Swiss Modernism 2.0,General,"Grid system, Helvetica, modular, asymmetric, international style, rational, clean, mathematical spacing","#000000, #FFFFFF, #F5F5F5, single vibrant accent only","Minimal secondary, accent for emphasis only, no gradients","display: grid, grid-template-columns: repeat(12 1fr), gap: 1rem, mathematical ratios, clear hierarchy","Corporate sites, architecture, editorial, SaaS, museums, professional services, documentation","Playful brands, children's sites, entertainment, gaming, emotional storytelling",✓ Full,✓ Full,⚡ Excellent,✓ WCAG AAA,✓ High,✓ High,"Tailwind 10/10, Bootstrap 9/10, Foundation 10/10",1950s Swiss + 2020s,Low
51,HUD / Sci-Fi FUI,General,"Futuristic, technical, wireframe, neon, data, transparency, iron man, sci-fi, interface","Neon Cyan #00FFFF, Holographic Blue #0080FF, Alert Red #FF0000","Transparent Black, Grid Lines #333333","Glow effects, scanning animations, ticker text, blinking markers, fine line drawing","Sci-fi games, space tech, cybersecurity, movie props, immersive dashboards","Standard corporate, reading heavy content, accessible public services",✓ Low,✓ Full,⚠ Moderate (renders),⚠ Poor (thin lines),◐ Medium,✗ Low,"React 9/10, Canvas 10/10",2010s Sci-Fi,High 51,HUD / Sci-Fi FUI,General,"Futuristic, technical, wireframe, neon, data, transparency, iron man, sci-fi, interface","Neon Cyan #00FFFF, Holographic Blue #0080FF, Alert Red #FF0000","Transparent Black, Grid Lines #333333","Glow effects, scanning animations, ticker text, blinking markers, fine line drawing","Sci-fi games, space tech, cybersecurity, movie props, immersive dashboards","Standard corporate, reading heavy content, accessible public services",✓ Low,✓ Full,⚠ Moderate (renders),⚠ Poor (thin lines),◐ Medium,✗ Low,"React 9/10, Canvas 10/10",2010s Sci-Fi,High
52,Pixel Art,General,"Retro, 8-bit, 16-bit, gaming, blocky, nostalgic, pixelated, arcade","Primary colors (NES Palette), brights, limited palette","Black outlines, shading via dithering or block colors","Frame-by-frame sprite animation, blinking cursor, instant transitions, marquee text","Indie games, retro tools, creative portfolios, nostalgia marketing, Web3/NFT","Professional corporate, modern SaaS, high-res photography sites",✓ Full,✓ Full,⚡ Excellent,✓ Good (if contrast ok),✓ High,◐ Medium,"CSS (box-shadow) 8/10, Canvas 10/10",1980s Arcade,Medium 52,Pixel Art,General,"Retro, 8-bit, 16-bit, gaming, blocky, nostalgic, pixelated, arcade","Primary colors (NES Palette), brights, limited palette","Black outlines, shading via dithering or block colors","Frame-by-frame sprite animation, blinking cursor, instant transitions, marquee text","Indie games, retro tools, creative portfolios, nostalgia marketing, Web3/NFT","Professional corporate, modern SaaS, high-res photography sites",✓ Full,✓ Full,⚡ Excellent,✓ Good (if contrast ok),✓ High,◐ Medium,"CSS (box-shadow) 8/10, Canvas 10/10",1980s Arcade,Medium
53,Bento Grids,General,"Apple-style, modular, cards, organized, clean, hierarchy, grid, rounded, soft","Off-white #F5F5F7, Clean White #FFFFFF, Text #1D1D1F","Subtle accents, soft shadows, blurred backdrops","Hover scale (1.02), soft shadow expansion, smooth layout shifts, content reveal","Product features, dashboards, personal sites, marketing summaries, galleries","Long-form reading, data tables, complex forms",✓ Full,✓ Full,⚡ Excellent,✓ WCAG AA,✓ High,✓ High,"CSS Grid 10/10, Tailwind 10/10",2020s Apple/Linear,Low 53,Bento Grids,General,"Apple-style, modular, cards, organized, clean, hierarchy, grid, rounded, soft","Off-white #F5F5F7, Clean White #FFFFFF, Text #1D1D1F","Subtle accents, soft shadows, blurred backdrops","Hover scale (1.02), soft shadow expansion, smooth layout shifts, content reveal","Product features, dashboards, personal sites, marketing summaries, galleries","Long-form reading, data tables, complex forms",✓ Full,✓ Full,⚡ Excellent,✓ WCAG AA,✓ High,✓ High,"CSS Grid 10/10, Tailwind 10/10",2020s Apple/Linear,Low
54,Neubrutalism,General,"Bold, ugly-cute, raw, high contrast, flat, hard shadows, distinct, playful, loud","Pop Yellow #FFDE59, Bright Red #FF5757, Black #000000","Lavender #CBA6F7, Mint #76E0C2","Hard hover shifts (4px), marquee scrolling, jitter animations, bold borders","Design tools, creative agencies, Gen Z brands, personal blogs, gumroad-style","Banking, legal, healthcare, serious enterprise, elderly users",✓ Full,✓ Full,⚡ Excellent,✓ WCAG AAA,✓ High,✓ High,"Tailwind 10/10, Plain CSS 10/10",2020s Modern Retro,Low 54,Neubrutalism,General,"Bold, ugly-cute, raw, high contrast, flat, hard shadows, distinct, playful, loud","Pop Yellow #FFDE59, Bright Red #FF5757, Black #000000","Lavender #CBA6F7, Mint #76E0C2","Hard hover shifts (4px), marquee scrolling, jitter animations, bold borders","Design tools, creative agencies, Gen Z brands, personal blogs, gumroad-style","Banking, legal, healthcare, serious enterprise, elderly users",✓ Full,✓ Full,⚡ Excellent,✓ WCAG AAA,✓ High,✓ High,"Tailwind 10/10, Plain CSS 10/10",2020s Modern Retro,Low
55,Spatial UI (VisionOS),General,"Glass, depth, immersion, spatial, translucent, gaze, gesture, apple, vision-pro","Frosted Glass #FFFFFF (15-30% opacity), System White","Vibrant system colors for active states, deep shadows for depth","Parallax depth, dynamic lighting response, gaze-hover effects, smooth scale on focus","Spatial computing apps, VR/AR interfaces, immersive media, futuristic dashboards","Text-heavy documents, high-contrast requirements, non-3D capable devices",✓ Full,✓ Full,⚠ Moderate (blur cost),⚠ Contrast risks,✓ High (if adapted),✓ High,"SwiftUI, React (Three.js/Fiber)",2024 Spatial Era,High 55,Spatial UI (VisionOS),General,"Glass, depth, immersion, spatial, translucent, gaze, gesture, apple, vision-pro","Frosted Glass #FFFFFF (15-30% opacity), System White","Vibrant system colors for active states, deep shadows for depth","Parallax depth, dynamic lighting response, gaze-hover effects, smooth scale on focus","Spatial computing apps, VR/AR interfaces, immersive media, futuristic dashboards","Text-heavy documents, high-contrast requirements, non-3D capable devices",✓ Full,✓ Full,⚠ Moderate (blur cost),⚠ Contrast risks,✓ High (if adapted),✓ High,"SwiftUI, React (Three.js/Fiber)",2024 Spatial Era,High
56,E-Ink / Paper,General,"Paper-like, matte, high contrast, texture, reading, calm, slow tech, monochrome","Off-White #FDFBF7, Paper White #F5F5F5, Ink Black #1A1A1A","Pencil Grey #4A4A4A, Highlighter Yellow #FFFF00 (accent)","No motion blur, distinct page turns, grain/noise texture, sharp transitions (no fade)","Reading apps, digital newspapers, minimal journals, distraction-free writing, slow-living brands","Gaming, video platforms, high-energy marketing, dark mode dependent apps",✓ Full,✗ Low (inverted only),⚡ Excellent,✓ WCAG AAA,✓ High,✓ Medium,"Tailwind 10/10, CSS 10/10",2020s Digital Well-being,Low 56,E-Ink / Paper,General,"Paper-like, matte, high contrast, texture, reading, calm, slow tech, monochrome","Off-White #FDFBF7, Paper White #F5F5F5, Ink Black #1A1A1A","Pencil Grey #4A4A4A, Highlighter Yellow #FFFF00 (accent)","No motion blur, distinct page turns, grain/noise texture, sharp transitions (no fade)","Reading apps, digital newspapers, minimal journals, distraction-free writing, slow-living brands","Gaming, video platforms, high-energy marketing, dark mode dependent apps",✓ Full,✗ Low (inverted only),⚡ Excellent,✓ WCAG AAA,✓ High,✓ Medium,"Tailwind 10/10, CSS 10/10",2020s Digital Well-being,Low
57,Gen Z Chaos / Maximalism,General,"Chaos, clutter, stickers, raw, collage, mixed media, loud, internet culture, ironic","Clashing Brights: #FF00FF, #00FF00, #FFFF00, #0000FF","Gradients, rainbow, glitch, noise, heavily saturated mix","Marquee scrolls, jitter, sticker layering, GIF overload, random placement, drag-and-drop","Gen Z lifestyle brands, music artists, creative portfolios, viral marketing, fashion","Corporate, government, healthcare, banking, serious tools",✓ Full,✓ Full,⚠ Poor (heavy assets),❌ Poor,◐ Medium,✓ High (Viral),CSS-in-JS 8/10,2023+ Internet Core,High 57,Gen Z Chaos / Maximalism,General,"Chaos, clutter, stickers, raw, collage, mixed media, loud, internet culture, ironic","Clashing Brights: #FF00FF, #00FF00, #FFFF00, #0000FF","Gradients, rainbow, glitch, noise, heavily saturated mix","Marquee scrolls, jitter, sticker layering, GIF overload, random placement, drag-and-drop","Gen Z lifestyle brands, music artists, creative portfolios, viral marketing, fashion","Corporate, government, healthcare, banking, serious tools",✓ Full,✓ Full,⚠ Poor (heavy assets),❌ Poor,◐ Medium,✓ High (Viral),CSS-in-JS 8/10,2023+ Internet Core,High
58,Biomimetic / Organic 2.0,General,"Nature-inspired, cellular, fluid, breathing, generative, algorithms, life-like","Cellular Pink #FF9999, Chlorophyll Green #00FF41, Bioluminescent Blue","Deep Ocean #001E3C, Coral #FF7F50, Organic gradients","Breathing animations, fluid morphing, generative growth, physics-based movement","Sustainability tech, biotech, advanced health, meditation, generative art platforms","Standard SaaS, data grids, strict corporate, accounting",✓ Full,✓ Full,⚠ Moderate,✓ Good,✓ Good,✓ High,"Canvas 10/10, WebGL 10/10",2024+ Generative,High 58,Biomimetic / Organic 2.0,General,"Nature-inspired, cellular, fluid, breathing, generative, algorithms, life-like","Cellular Pink #FF9999, Chlorophyll Green #00FF41, Bioluminescent Blue","Deep Ocean #001E3C, Coral #FF7F50, Organic gradients","Breathing animations, fluid morphing, generative growth, physics-based movement","Sustainability tech, biotech, advanced health, meditation, generative art platforms","Standard SaaS, data grids, strict corporate, accounting",✓ Full,✓ Full,⚠ Moderate,✓ Good,✓ Good,✓ High,"Canvas 10/10, WebGL 10/10",2024+ Generative,High
1 STT Style Category Type Keywords Primary Colors Secondary Colors Effects & Animation Best For Do Not Use For Light Mode ✓ Dark Mode ✓ Performance Accessibility Mobile-Friendly Conversion-Focused Framework Compatibility Era/Origin Complexity
2 1 Minimalism & Swiss Style General Clean, simple, spacious, functional, white space, high contrast, geometric, sans-serif, grid-based, essential Monochromatic, Black #000000, White #FFFFFF Neutral (Beige #F5F1E8, Grey #808080, Taupe #B38B6D), Primary accent Subtle hover (200-250ms), smooth transitions, sharp shadows if any, clear type hierarchy, fast loading Enterprise apps, dashboards, documentation sites, SaaS platforms, professional tools Creative portfolios, entertainment, playful brands, artistic experiments ✓ Full ✓ Full ⚡ Excellent ✓ WCAG AAA ✓ High ◐ Medium Tailwind 10/10, Bootstrap 9/10, MUI 9/10 1950s Swiss Low
3 2 Neumorphism General Soft UI, embossed, debossed, convex, concave, light source, subtle depth, rounded (12-16px), monochromatic Light pastels: Soft Blue #C8E0F4, Soft Pink #F5E0E8, Soft Grey #E8E8E8 Tints/shades (±30%), gradient subtlety, color harmony Soft box-shadow (multiple: -5px -5px 15px, 5px 5px 15px), smooth press (150ms), inner subtle shadow Health/wellness apps, meditation platforms, fitness trackers, minimal interaction UIs Complex apps, critical accessibility, data-heavy dashboards, high-contrast required ✓ Full ◐ Partial ⚡ Good ⚠ Low contrast ✓ Good ◐ Medium Tailwind 8/10, CSS-in-JS 9/10 2020s Modern Medium
4 3 Glassmorphism General Frosted glass, transparent, blurred background, layered, vibrant background, light source, depth, multi-layer Translucent white: rgba(255,255,255,0.1-0.3) Vibrant: Electric Blue #0080FF, Neon Purple #8B00FF, Vivid Pink #FF1493, Teal #20B2AA Backdrop blur (10-20px), subtle border (1px solid rgba white 0.2), light reflection, Z-depth Modern SaaS, financial dashboards, high-end corporate, lifestyle apps, modal overlays, navigation Low-contrast backgrounds, critical accessibility, performance-limited, dark text on dark ✓ Full ✓ Full ⚠ Good ⚠ Ensure 4.5:1 ✓ Good ✓ High Tailwind 9/10, MUI 8/10, Chakra 8/10 2020s Modern Medium
5 4 Brutalism General Raw, unpolished, stark, high contrast, plain text, default fonts, visible borders, asymmetric, anti-design Primary: Red #FF0000, Blue #0000FF, Yellow #FFFF00, Black #000000, White #FFFFFF Limited: Neon Green #00FF00, Hot Pink #FF00FF, minimal secondary No smooth transitions (instant), sharp corners (0px), bold typography (700+), visible grid, large blocks Design portfolios, artistic projects, counter-culture brands, editorial/media sites, tech blogs Corporate environments, conservative industries, critical accessibility, customer-facing professional ✓ Full ✓ Full ⚡ Excellent ✓ WCAG AAA ◐ Medium ✗ Low Tailwind 10/10, Bootstrap 7/10 1950s Brutalist Low
6 5 3D & Hyperrealism General Depth, realistic textures, 3D models, spatial navigation, tactile, skeuomorphic elements, rich detail, immersive Deep Navy #001F3F, Forest Green #228B22, Burgundy #800020, Gold #FFD700, Silver #C0C0C0 Complex gradients (5-10 stops), realistic lighting, shadow variations (20-40% darker) WebGL/Three.js 3D, realistic shadows (layers), physics lighting, parallax (3-5 layers), smooth 3D (300-400ms) Gaming, product showcase, immersive experiences, high-end e-commerce, architectural viz, VR/AR Low-end mobile, performance-limited, critical accessibility, data tables/forms ◐ Partial ◐ Partial ❌ Poor ⚠ Not accessible ✗ Low ◐ Medium Three.js 10/10, R3F 10/10, Babylon.js 10/10 2020s Modern High
7 6 Vibrant & Block-based General Bold, energetic, playful, block layout, geometric shapes, high color contrast, duotone, modern, energetic Neon Green #39FF14, Electric Purple #BF00FF, Vivid Pink #FF1493, Bright Cyan #00FFFF, Sunburst #FFAA00 Complementary: Orange #FF7F00, Shocking Pink #FF006E, Lime #CCFF00, triadic schemes Large sections (48px+ gaps), animated patterns, bold hover (color shift), scroll-snap, large type (32px+), 200-300ms Startups, creative agencies, gaming, social media, youth-focused, entertainment, consumer Financial institutions, healthcare, formal business, government, conservative, elderly ✓ Full ✓ Full ⚡ Good ◐ Ensure WCAG ✓ High ✓ High Tailwind 10/10, Chakra 9/10, Styled 9/10 2020s Modern Medium
8 7 Dark Mode (OLED) General Dark theme, low light, high contrast, deep black, midnight blue, eye-friendly, OLED, night mode, power efficient Deep Black #000000, Dark Grey #121212, Midnight Blue #0A0E27 Vibrant accents: Neon Green #39FF14, Electric Blue #0080FF, Gold #FFD700, Plasma Purple #BF00FF Minimal glow (text-shadow: 0 0 10px), dark-to-light transitions, low white emission, high readability, visible focus Night-mode apps, coding platforms, entertainment, eye-strain prevention, OLED devices, low-light Print-first content, high-brightness outdoor, color-accuracy-critical ✗ No ✓ Only ⚡ Excellent ✓ WCAG AAA ✓ High ◐ Low Tailwind 10/10, MUI 10/10, Chakra 10/10 2020s Modern Low
9 8 Accessible & Ethical General High contrast, large text (16px+), keyboard navigation, screen reader friendly, WCAG compliant, focus state, semantic WCAG AA/AAA (4.5:1 min), simple primary, clear secondary, high luminosity (7:1+) Symbol-based colors (not color-only), supporting patterns, inclusive combinations Clear focus rings (3-4px), ARIA labels, skip links, responsive design, reduced motion, 44x44px touch targets Government, healthcare, education, inclusive products, large audience, legal compliance, public None - accessibility universal ✓ Full ✓ Full ⚡ Excellent ✓ WCAG AAA ✓ High ✓ High All frameworks 10/10 Universal Low
10 9 Claymorphism General Soft 3D, chunky, playful, toy-like, bubbly, thick borders (3-4px), double shadows, rounded (16-24px) Pastel: Soft Peach #FDBCB4, Baby Blue #ADD8E6, Mint #98FF98, Lilac #E6E6FA, light BG Soft gradients (pastel-to-pastel), light/dark variations (20-30%), gradient subtle Inner+outer shadows (subtle, no hard lines), soft press (200ms ease-out), fluffy elements, smooth transitions Educational apps, children's apps, SaaS platforms, creative tools, fun-focused, onboarding, casual games Formal corporate, professional services, data-critical, serious/medical, legal apps, finance ✓ Full ◐ Partial ⚡ Good ⚠ Ensure 4.5:1 ✓ High ✓ High Tailwind 9/10, CSS-in-JS 9/10 2020s Modern Medium
11 10 Aurora UI General Vibrant gradients, smooth blend, Northern Lights effect, mesh gradient, luminous, atmospheric, abstract Complementary: Blue-Orange, Purple-Yellow, Electric Blue #0080FF, Magenta #FF1493, Cyan #00FFFF Smooth transitions (Blue→Purple→Pink→Teal), iridescent effects, blend modes (screen, multiply) Large flowing CSS/SVG gradients, subtle 8-12s animations, depth via color layering, smooth morph Modern SaaS, creative agencies, branding, music platforms, lifestyle, premium products, hero sections Data-heavy dashboards, critical accessibility, content-heavy where distraction issues ✓ Full ✓ Full ⚠ Good ⚠ Text contrast ✓ Good ✓ High Tailwind 9/10, CSS-in-JS 10/10 2020s Modern Medium
12 11 Retro-Futurism General Vintage sci-fi, 80s aesthetic, neon glow, geometric patterns, CRT scanlines, pixel art, cyberpunk, synthwave Neon Blue #0080FF, Hot Pink #FF006E, Cyan #00FFFF, Deep Black #1A1A2E, Purple #5D34D0 Metallic Silver #C0C0C0, Gold #FFD700, duotone, 80s Pink #FF10F0, neon accents CRT scanlines (::before overlay), neon glow (text-shadow+box-shadow), glitch effects (skew/offset keyframes) Gaming, entertainment, music platforms, tech brands, artistic projects, nostalgic, cyberpunk Conservative industries, critical accessibility, professional/corporate, elderly, legal/finance ✓ Full ✓ Dark focused ⚠ Moderate ⚠ High contrast/strain ◐ Medium ◐ Medium Tailwind 8/10, CSS-in-JS 9/10 1980s Retro Medium
13 12 Flat Design General 2D, minimalist, bold colors, no shadows, clean lines, simple shapes, typography-focused, modern, icon-heavy Solid bright: Red, Orange, Blue, Green, limited palette (4-6 max) Complementary colors, muted secondaries, high saturation, clean accents No gradients/shadows, simple hover (color/opacity shift), fast loading, clean transitions (150-200ms ease), minimal icons Web apps, mobile apps, cross-platform, startup MVPs, user-friendly, SaaS, dashboards, corporate Complex 3D, premium/luxury, artistic portfolios, immersive experiences, high-detail ✓ Full ✓ Full ⚡ Excellent ✓ WCAG AAA ✓ High ✓ High Tailwind 10/10, Bootstrap 10/10, MUI 9/10 2010s Modern Low
14 13 Skeuomorphism General Realistic, texture, depth, 3D appearance, real-world metaphors, shadows, gradients, tactile, detailed, material Rich realistic: wood, leather, metal colors, detailed gradients (8-12 stops), metallic effects Realistic lighting gradients, shadow variations (30-50% darker), texture overlays, material colors Realistic shadows (layers), depth (perspective), texture details (noise, grain), realistic animations (300-500ms) Legacy apps, gaming, immersive storytelling, premium products, luxury, realistic simulations, education Modern enterprise, critical accessibility, low-performance, web (use Flat/Modern) ◐ Partial ◐ Partial ❌ Poor ⚠ Textures reduce readability ✗ Low ◐ Medium CSS-in-JS 7/10, Custom 8/10 2007-2012 iOS High
15 14 Liquid Glass General Flowing glass, morphing, smooth transitions, fluid effects, translucent, animated blur, iridescent, chromatic aberration Vibrant iridescent (rainbow spectrum), translucent base with opacity shifts, gradient fluidity Chromatic aberration (Red-Cyan), iridescent oil-spill, fluid gradient blends, holographic effects Morphing elements (SVG/CSS), fluid animations (400-600ms curves), dynamic blur (backdrop-filter), color transitions Premium SaaS, high-end e-commerce, creative platforms, branding experiences, luxury portfolios Performance-limited, critical accessibility, complex data, budget projects ✓ Full ✓ Full ⚠ Moderate-Poor ⚠ Text contrast ◐ Medium ✓ High Framer Motion 10/10, GSAP 10/10 2020s Modern High
16 15 Motion-Driven General Animation-heavy, microinteractions, smooth transitions, scroll effects, parallax, entrance anim, page transitions Bold colors emphasize movement, high contrast animated, dynamic gradients, accent action colors Transitional states, success (Green #22C55E), error (Red #EF4444), neutral feedback Scroll anim (Intersection Observer), hover (300-400ms), entrance, parallax (3-5 layers), page transitions Portfolio sites, storytelling platforms, interactive experiences, entertainment apps, creative, SaaS Data dashboards, critical accessibility, low-power devices, content-heavy, motion-sensitive ✓ Full ✓ Full ⚠ Good ⚠ Prefers-reduced-motion ✓ Good ✓ High GSAP 10/10, Framer Motion 10/10 2020s Modern High
17 16 Micro-interactions General Small animations, gesture-based, tactile feedback, subtle animations, contextual interactions, responsive Subtle color shifts (10-20%), feedback: Green #22C55E, Red #EF4444, Amber #F59E0B Accent feedback, neutral supporting, clear action indicators Small hover (50-100ms), loading spinners, success/error state anim, gesture-triggered (swipe/pinch), haptic Mobile apps, touchscreen UIs, productivity tools, user-friendly, consumer apps, interactive components Desktop-only, critical performance, accessibility-first (alternatives needed) ✓ Full ✓ Full ⚡ Excellent ✓ Good ✓ High ✓ High Framer Motion 10/10, React Spring 9/10 2020s Modern Medium
18 17 Inclusive Design General Accessible, color-blind friendly, high contrast, haptic feedback, voice interaction, screen reader, WCAG AAA, universal WCAG AAA (7:1+ contrast), avoid red-green only, symbol-based indicators, high contrast primary Supporting patterns (stripes, dots, hatch), symbols, combinations, clear non-color indicators Haptic feedback (vibration), voice guidance, focus indicators (4px+ ring), motion options, alt content, semantic Public services, education, healthcare, finance, government, accessible consumer, inclusive None - accessibility universal ✓ Full ✓ Full ⚡ Excellent ✓ WCAG AAA ✓ High ✓ High All frameworks 10/10 Universal Low
19 18 Zero Interface General Minimal visible UI, voice-first, gesture-based, AI-driven, invisible controls, predictive, context-aware, ambient Neutral backgrounds: Soft white #FAFAFA, light grey #F0F0F0, warm off-white #F5F1E8 Subtle feedback: light green, light red, minimal UI elements, soft accents Voice recognition UI, gesture detection, AI predictions (smooth reveal), progressive disclosure, smart suggestions Voice assistants, AI platforms, future-forward UX, smart home, contextual computing, ambient experiences Complex workflows, data-entry heavy, traditional systems, legacy support, explicit control ✓ Full ✓ Full ⚡ Excellent ✓ Excellent ✓ High ✓ High Tailwind 10/10, Custom 10/10 2020s AI-Era Low
20 19 Soft UI Evolution General Evolved soft UI, better contrast, modern aesthetics, subtle depth, accessibility-focused, improved shadows, hybrid Improved contrast pastels: Soft Blue #87CEEB, Soft Pink #FFB6C1, Soft Green #90EE90, better hierarchy Better combinations, accessible secondary, supporting with improved contrast, modern accents Improved shadows (softer than flat, clearer than neumorphism), modern (200-300ms), focus visible, WCAG AA/AAA Modern enterprise apps, SaaS platforms, health/wellness, modern business tools, professional, hybrid Extreme minimalism, critical performance, systems without modern OS ✓ Full ✓ Full ⚡ Excellent ✓ WCAG AA+ ✓ High ✓ High Tailwind 9/10, MUI 9/10, Chakra 9/10 2020s Modern Medium
21 20 Hero-Centric Design Landing Page Large hero section, compelling headline, high-contrast CTA, product showcase, value proposition, hero image/video, dramatic visual Brand primary color, white/light backgrounds for contrast, accent color for CTA Supporting colors for secondary CTAs, accent highlights, trust elements (testimonials, logos) Smooth scroll reveal, fade-in animations on hero, subtle background parallax, CTA glow/pulse effect SaaS landing pages, product launches, service landing pages, B2B platforms, tech companies Complex navigation, multi-page experiences, data-heavy applications ✓ Full ✓ Full ⚡ Good ✓ WCAG AA ✓ Full ✓ Very High Tailwind 10/10, Bootstrap 9/10 2020s Modern Medium
22 21 Conversion-Optimized Landing Page Form-focused, minimalist design, single CTA focus, high contrast, urgency elements, trust signals, social proof, clear value Primary brand color, high-contrast white/light backgrounds, warning/urgency colors for time-limited offers Secondary CTA color (muted), trust element colors (testimonial highlights), accent for key benefits Hover states on CTA (color shift, slight scale), form field focus animations, loading spinner, success feedback E-commerce product pages, free trial signups, lead generation, SaaS pricing pages, limited-time offers Complex feature explanations, multi-product showcases, technical documentation ✓ Full ✓ Full ⚡ Excellent ✓ WCAG AA ✓ Full (mobile-optimized) ✓ Very High
23 22 Feature-Rich Showcase Landing Page Multiple feature sections, grid layout, benefit cards, visual feature demonstrations, interactive elements, problem-solution pairs Primary brand, bright secondary colors for feature cards, contrasting accent for CTAs Supporting colors for: benefits (green), problems (red/orange), features (blue/purple), social proof (neutral) Card hover effects (lift/scale), icon animations on scroll, feature toggle animations, smooth section transitions Enterprise SaaS, software tools landing pages, platform services, complex product explanations, B2B products Simple product pages, early-stage startups with few features, entertainment landing pages ✓ Full ✓ Full ⚡ Good ✓ WCAG AA ✓ Good ✓ High
24 23 Minimal & Direct Landing Page Minimal text, white space heavy, single column layout, direct messaging, clean typography, visual-centric, fast-loading Monochromatic primary, white background, single accent color for CTA, black/dark grey text Minimal secondary colors, reserved for critical CTAs only, neutral supporting elements Very subtle hover effects, minimal animations, fast page load (no heavy animations), smooth scroll Simple service landing pages, indie products, consulting services, micro SaaS, freelancer portfolios Feature-heavy products, complex explanations, multi-product showcases ✓ Full ✓ Full ⚡ Excellent ✓ WCAG AAA ✓ Full ✓ High
25 24 Social Proof-Focused Landing Page Testimonials prominent, client logos displayed, case studies sections, reviews/ratings, user avatars, success metrics, credibility markers Primary brand, trust colors (blue), success/growth colors (green), neutral backgrounds Testimonial highlight colors, logo grid backgrounds (light grey), badge/achievement colors Testimonial carousel animations, logo grid fade-in, stat counter animations (number count-up), review star ratings B2B SaaS, professional services, premium products, e-commerce conversion pages, established brands Startup MVPs, products without users, niche/experimental products ✓ Full ✓ Full ⚡ Good ✓ WCAG AA ✓ Full ✓ High
26 25 Interactive Product Demo Landing Page Embedded product mockup/video, interactive elements, product walkthrough, step-by-step guides, hover-to-reveal features, embedded demos Primary brand, interface colors matching product, demo highlight colors for interactive elements Product UI colors, tutorial step colors (numbered progression), hover state indicators Product animation playback, step progression animations, hover reveal effects, smooth zoom on interaction SaaS platforms, tool/software products, productivity apps landing pages, developer tools, productivity software Simple services, consulting, non-digital products, complexity-averse audiences ✓ Full ✓ Full ⚠ Good (video/interactive) ✓ WCAG AA ✓ Good ✓ Very High
27 26 Trust & Authority Landing Page Certificates/badges displayed, expert credentials, case studies with metrics, before/after comparisons, industry recognition, security badges Professional colors (blue/grey), trust colors, certification badge colors (gold/silver accents) Certificate highlight colors, metric showcase colors, comparison highlight (success green) Badge hover effects, metric pulse animations, certificate carousel, smooth stat reveal Healthcare/medical landing pages, financial services, enterprise software, premium/luxury products, legal services Casual products, entertainment, viral/social-first products ✓ Full ✓ Full ⚡ Excellent ✓ WCAG AAA ✓ Full ✓ High
28 27 Storytelling-Driven Landing Page Narrative flow, visual story progression, section transitions, consistent character/brand voice, emotional messaging, journey visualization Brand primary, warm/emotional colors, varied accent colors per story section, high visual variety Story section color coding, emotional state colors (calm, excitement, success), transitional gradients Section-to-section animations, scroll-triggered reveals, character/icon animations, morphing transitions, parallax narrative Brand/startup stories, mission-driven products, premium/lifestyle brands, documentary-style products, educational Technical/complex products (unless narrative-driven), traditional enterprise software ✓ Full ✓ Full ⚠ Moderate (animations) ✓ WCAG AA ✓ Good ✓ High
29 28 Data-Dense Dashboard BI/Analytics Multiple charts/widgets, data tables, KPI cards, minimal padding, grid layout, space-efficient, maximum data visibility Neutral primary (light grey/white #F5F5F5), data colors (blue/green/red), dark text #333333 Chart colors: success (green #22C55E), warning (amber #F59E0B), alert (red #EF4444), neutral (grey) Hover tooltips, chart zoom on click, row highlighting on hover, smooth filter animations, data loading spinners Business intelligence dashboards, financial analytics, enterprise reporting, operational dashboards, data warehousing Marketing dashboards, consumer-facing analytics, simple reporting ✓ Full ✓ Full ⚡ Excellent ✓ WCAG AA ◐ Medium ✗ Not applicable
30 29 Heat Map & Heatmap Style BI/Analytics Color-coded grid/matrix, data intensity visualization, geographical heat maps, correlation matrices, cell-based representation, gradient coloring Gradient scale: Cool (blue #0080FF) to hot (red #FF0000), neutral middle (white/yellow) Support gradients: Light (cool blue) to dark (warm red), divergent for positive/negative data, monochromatic options Color gradient transitions on data change, cell highlighting on hover, tooltip reveal on click, smooth color animation Geographical analysis, performance matrices, correlation analysis, user behavior heatmaps, temperature/intensity data Linear data representation, categorical comparisons (use bar charts), small datasets ✓ Full ✓ Full (with adjustments) ⚡ Excellent ⚠ Colorblind considerations ◐ Medium ✗ Not applicable
31 30 Executive Dashboard BI/Analytics High-level KPIs, large key metrics, minimal detail, summary view, trend indicators, at-a-glance insights, executive summary Brand colors, professional palette (blue/grey/white), accent for KPIs, red for alerts/concerns KPI highlight colors: positive (green), negative (red), neutral (grey), trend arrow colors KPI value animations (count-up), trend arrow direction animations, metric card hover lift, alert pulse effect C-suite dashboards, business summary reports, decision-maker dashboards, strategic planning views Detailed analyst dashboards, technical deep-dives, operational monitoring ✓ Full ✓ Full ⚡ Excellent ✓ WCAG AA ✗ Low (not mobile-optimized) ✗ Not applicable
32 31 Real-Time Monitoring BI/Analytics Live data updates, status indicators, alert notifications, streaming data visualization, active monitoring, streaming charts Alert colors: critical (red #FF0000), warning (orange #FFA500), normal (green #22C55E), updating (blue animation) Status indicator colors, chart line colors varying by metric, streaming data highlight colors Real-time chart animations, alert pulse/glow, status indicator blink animation, smooth data stream updates, loading effect System monitoring dashboards, DevOps dashboards, real-time analytics, stock market dashboards, live event tracking Historical analysis, long-term trend reports, archived data dashboards ✓ Full ✓ Full ⚡ Good (real-time load) ✓ WCAG AA ◐ Medium ✗ Not applicable
33 32 Drill-Down Analytics BI/Analytics Hierarchical data exploration, expandable sections, interactive drill-down paths, summary-to-detail flow, context preservation Primary brand, breadcrumb colors, drill-level indicator colors, hierarchy depth colors Drill-down path indicator colors, level-specific colors, highlight colors for selected level, transition colors Drill-down expand animations, breadcrumb click transitions, smooth detail reveal, level change smooth, data reload animation Sales analytics, product analytics, funnel analysis, multi-dimensional data exploration, business intelligence Simple linear data, single-metric dashboards, streaming real-time dashboards ✓ Full ✓ Full ⚡ Good ✓ WCAG AA ◐ Medium ✗ Not applicable
34 33 Comparative Analysis Dashboard BI/Analytics Side-by-side comparisons, period-over-period metrics, A/B test results, regional comparisons, performance benchmarks Comparison colors: primary (blue), comparison (orange/purple), delta indicator (green/red) Winning metric color (green), losing metric color (red), neutral comparison (grey), benchmark colors Comparison bar animations (grow to value), delta indicator animations (direction arrows), highlight on compare Period-over-period reporting, A/B test dashboards, market comparison, competitive analysis, regional performance Single metric dashboards, future projections (use forecasting), real-time only (no historical) ✓ Full ✓ Full ⚡ Excellent ✓ WCAG AA ◐ Medium ✗ Not applicable
35 34 Predictive Analytics BI/Analytics Forecast lines, confidence intervals, trend projections, scenario modeling, AI-driven insights, anomaly detection visualization Forecast line color (distinct from actual), confidence interval shading, anomaly highlight (red alert), trend colors High confidence (dark color), low confidence (light color), anomaly colors (red/orange), normal trend (green/blue) Forecast line animation on draw, confidence band fade-in, anomaly pulse alert, smoothing function animations Forecasting dashboards, anomaly detection systems, trend prediction dashboards, AI-powered analytics, budget planning Historical-only dashboards, simple reporting, real-time operational dashboards ✓ Full ✓ Full ⚠ Good (computation) ✓ WCAG AA ◐ Medium ✗ Not applicable
36 35 User Behavior Analytics BI/Analytics Funnel visualization, user flow diagrams, conversion tracking, engagement metrics, user journey mapping, cohort analysis Funnel stage colors: high engagement (green), drop-off (red), conversion (blue), user flow arrows (grey) Stage completion colors (success), abandonment colors (warning), engagement levels (gradient), cohort colors Funnel animation (fill-down), flow diagram animations (connection draw), conversion pulse, engagement bar fill Conversion funnel analysis, user journey tracking, engagement analytics, cohort analysis, retention tracking Real-time operational metrics, technical system monitoring, financial transactions ✓ Full ✓ Full ⚡ Good ✓ WCAG AA ✓ Good ✗ Not applicable
37 36 Financial Dashboard BI/Analytics Revenue metrics, profit/loss visualization, budget tracking, financial ratios, portfolio performance, cash flow, audit trail Financial colors: profit (green #22C55E), loss (red #EF4444), neutral (grey), trust (dark blue #003366) Revenue highlight (green), expenses (red), budget variance (orange/red), balance (grey), accuracy (blue) Number animations (count-up), trend direction indicators, percentage change animations, profit/loss color transitions Financial reporting, accounting dashboards, portfolio tracking, budget monitoring, banking analytics Simple business dashboards, entertainment/social metrics, non-financial data ✓ Full ✓ Full ⚡ Excellent ✓ WCAG AAA ✗ Low ✗ Not applicable
38 37 Sales Intelligence Dashboard BI/Analytics Deal pipeline, sales metrics, territory performance, sales rep leaderboard, win-loss analysis, quota tracking, forecast accuracy Sales colors: won (green), lost (red), in-progress (blue), blocked (orange), quota met (gold), quota missed (grey) Pipeline stage colors, rep performance colors, quota achievement colors, forecast accuracy colors Deal movement animations, metric updates, leaderboard ranking changes, gauge needle movements, status change highlights CRM dashboards, sales management, opportunity tracking, performance management, quota planning Marketing analytics, customer support metrics, HR dashboards ✓ Full ✓ Full ⚡ Good ✓ WCAG AA ◐ Medium ✗ Not applicable Recharts 9/10, Chart.js 9/10 2020s Modern Medium
39 38 Neubrutalism General Bold borders, black outlines, primary colors, thick shadows, no gradients, flat colors, 45° shadows, playful, Gen Z #FFEB3B (Yellow), #FF5252 (Red), #2196F3 (Blue), #000000 (Black borders) Limited accent colors, high contrast combinations, no gradients allowed box-shadow: 4px 4px 0 #000, border: 3px solid #000, no gradients, sharp corners (0px), bold typography Gen Z brands, startups, creative agencies, Figma-style apps, Notion-style interfaces, tech blogs Luxury brands, finance, healthcare, conservative industries (too playful) ✓ Full ✓ Full ⚡ Excellent ✓ WCAG AAA ✓ High ✓ High Tailwind 10/10, Bootstrap 8/10 2020s Modern Low
40 39 Bento Box Grid General Modular cards, asymmetric grid, varied sizes, Apple-style, dashboard tiles, negative space, clean hierarchy, cards Neutral base + brand accent, #FFFFFF, #F5F5F5, brand primary Subtle gradients, shadow variations, accent highlights for interactive cards grid-template with varied spans, rounded-xl (16px), subtle shadows, hover scale (1.02), smooth transitions Dashboards, product pages, portfolios, Apple-style marketing, feature showcases, SaaS Dense data tables, text-heavy content, real-time monitoring ✓ Full ✓ Full ⚡ Excellent ✓ WCAG AA ✓ High ✓ High Tailwind 10/10, CSS Grid 10/10 2020s Apple Low
41 40 Y2K Aesthetic General Neon pink, chrome, metallic, bubblegum, iridescent, glossy, retro-futurism, 2000s, futuristic nostalgia #FF69B4 (Hot Pink), #00FFFF (Cyan), #C0C0C0 (Silver), #9400D3 (Purple) Metallic gradients, glossy overlays, iridescent effects, chrome textures linear-gradient metallic, glossy buttons, 3D chrome effects, glow animations, bubble shapes Fashion brands, music platforms, Gen Z brands, nostalgia marketing, entertainment, youth-focused B2B enterprise, healthcare, finance, conservative industries, elderly users ✓ Full ◐ Partial ⚠ Good ⚠ Check contrast ✓ Good ✓ High Tailwind 8/10, CSS-in-JS 9/10 Y2K 2000s Medium
42 41 Cyberpunk UI General Neon, dark mode, terminal, HUD, sci-fi, glitch, dystopian, futuristic, matrix, tech noir #00FF00 (Matrix Green), #FF00FF (Magenta), #00FFFF (Cyan), #0D0D0D (Dark) Neon gradients, scanline overlays, glitch colors, terminal green accents Neon glow (text-shadow), glitch animations (skew/offset), scanlines (::before overlay), terminal fonts Gaming platforms, tech products, crypto apps, sci-fi applications, developer tools, entertainment Corporate enterprise, healthcare, family apps, conservative brands, elderly users ✗ No ✓ Only ⚠ Moderate ⚠ Limited (dark+neon) ◐ Medium ◐ Medium Tailwind 8/10, Custom CSS 10/10 2020s Cyberpunk Medium
43 42 Organic Biophilic General Nature, organic shapes, green, sustainable, rounded, flowing, wellness, earthy, natural textures #228B22 (Forest Green), #8B4513 (Earth Brown), #87CEEB (Sky Blue), #F5F5DC (Beige) Natural gradients, earth tones, sky blues, organic textures, wood/stone colors Rounded corners (16-24px), organic curves (border-radius variations), natural shadows, flowing SVG shapes Wellness apps, sustainability brands, eco products, health apps, meditation, organic food brands Tech-focused products, gaming, industrial, urban brands ✓ Full ✓ Full ⚡ Excellent ✓ WCAG AA ✓ High ✓ High Tailwind 10/10, CSS 10/10 2020s Sustainable Low
44 43 AI-Native UI General Chatbot, conversational, voice, assistant, agentic, ambient, minimal chrome, streaming text, AI interactions Neutral + single accent, #6366F1 (AI Purple), #10B981 (Success), #F5F5F5 (Background) Status indicators, streaming highlights, context card colors, subtle accent variations Typing indicators (3-dot pulse), streaming text animations, pulse animations, context cards, smooth reveals AI products, chatbots, voice assistants, copilots, AI-powered tools, conversational interfaces Traditional forms, data-heavy dashboards, print-first content ✓ Full ✓ Full ⚡ Excellent ✓ WCAG AA ✓ High ✓ High Tailwind 10/10, React 10/10 2020s AI-Era Low
45 44 Memphis Design General 80s, geometric, playful, postmodern, shapes, patterns, squiggles, triangles, neon, abstract, bold #FF71CE (Hot Pink), #FFCE5C (Yellow), #86CCCA (Teal), #6A7BB4 (Blue Purple) Complementary geometric colors, pattern fills, contrasting accent shapes transform: rotate(), clip-path: polygon(), mix-blend-mode, repeating patterns, bold shapes Creative agencies, music sites, youth brands, event promotion, artistic portfolios, entertainment Corporate finance, healthcare, legal, elderly users, conservative brands ✓ Full ✓ Full ⚡ Excellent ⚠ Check contrast ✓ Good ◐ Medium Tailwind 9/10, CSS 10/10 1980s Postmodern Medium
46 45 Vaporwave General Synthwave, retro-futuristic, 80s-90s, neon, glitch, nostalgic, sunset gradient, dreamy, aesthetic #FF71CE (Pink), #01CDFE (Cyan), #05FFA1 (Mint), #B967FF (Purple) Sunset gradients, glitch overlays, VHS effects, neon accents, pastel variations text-shadow glow, linear-gradient, filter: hue-rotate(), glitch animations, retro scan lines Music platforms, gaming, creative portfolios, tech startups, entertainment, artistic projects Business apps, e-commerce, education, healthcare, enterprise software ✓ Full ✓ Dark focused ⚠ Moderate ⚠ Poor (motion) ◐ Medium ◐ Medium Tailwind 8/10, CSS-in-JS 9/10 1980s-90s Retro Medium
47 46 Dimensional Layering General Depth, overlapping, z-index, layers, 3D, shadows, elevation, floating, cards, spatial hierarchy Neutral base (#FFFFFF, #F5F5F5, #E0E0E0) + brand accent for elevated elements Shadow variations (sm/md/lg/xl), elevation colors, highlight colors for top layers z-index stacking, box-shadow elevation (4 levels), transform: translateZ(), backdrop-filter, parallax Dashboards, card layouts, modals, navigation, product showcases, SaaS interfaces Print-style layouts, simple blogs, low-end devices, flat design requirements ✓ Full ✓ Full ⚠ Good ⚠ Moderate (SR issues) ✓ Good ✓ High Tailwind 10/10, MUI 10/10, Chakra 10/10 2020s Modern Medium
48 47 Exaggerated Minimalism General Bold minimalism, oversized typography, high contrast, negative space, loud minimal, statement design #000000 (Black), #FFFFFF (White), single vibrant accent only Minimal - single accent color, no secondary colors, extreme restraint font-size: clamp(3rem 10vw 12rem), font-weight: 900, letter-spacing: -0.05em, massive whitespace Fashion, architecture, portfolios, agency landing pages, luxury brands, editorial E-commerce catalogs, dashboards, forms, data-heavy, elderly users, complex apps ✓ Full ✓ Full ⚡ Excellent ✓ WCAG AA ✓ High ✓ High Tailwind 10/10, Typography.js 10/10 2020s Modern Low
49 48 Kinetic Typography General Motion text, animated type, moving letters, dynamic, typing effect, morphing, scroll-triggered text Flexible - high contrast recommended, bold colors for emphasis, animation-friendly palette Accent colors for emphasis, transition colors, gradient text fills @keyframes text animation, typing effect, background-clip: text, GSAP ScrollTrigger, split text Hero sections, marketing sites, video platforms, storytelling, creative portfolios, landing pages Long-form content, accessibility-critical, data interfaces, forms, elderly users ✓ Full ✓ Full ⚠ Moderate ❌ Poor (motion) ✓ Good ✓ Very High GSAP 10/10, Framer Motion 10/10 2020s Modern High
50 49 Parallax Storytelling General Scroll-driven, narrative, layered scrolling, immersive, progressive disclosure, cinematic, scroll-triggered Story-dependent, often gradients and natural colors, section-specific palettes Section transition colors, depth layer colors, narrative mood colors transform: translateY(scroll), position: fixed/sticky, perspective: 1px, scroll-triggered animations Brand storytelling, product launches, case studies, portfolios, annual reports, marketing campaigns E-commerce, dashboards, mobile-first, SEO-critical, accessibility-required ✓ Full ✓ Full ❌ Poor ❌ Poor (motion) ✗ Low ✓ High GSAP ScrollTrigger 10/10, Locomotive Scroll 10/10 2020s Modern High
51 50 Swiss Modernism 2.0 General Grid system, Helvetica, modular, asymmetric, international style, rational, clean, mathematical spacing #000000, #FFFFFF, #F5F5F5, single vibrant accent only Minimal secondary, accent for emphasis only, no gradients display: grid, grid-template-columns: repeat(12 1fr), gap: 1rem, mathematical ratios, clear hierarchy Corporate sites, architecture, editorial, SaaS, museums, professional services, documentation Playful brands, children's sites, entertainment, gaming, emotional storytelling ✓ Full ✓ Full ⚡ Excellent ✓ WCAG AAA ✓ High ✓ High Tailwind 10/10, Bootstrap 9/10, Foundation 10/10 1950s Swiss + 2020s Low
52 51 HUD / Sci-Fi FUI General Futuristic, technical, wireframe, neon, data, transparency, iron man, sci-fi, interface Neon Cyan #00FFFF, Holographic Blue #0080FF, Alert Red #FF0000 Transparent Black, Grid Lines #333333 Glow effects, scanning animations, ticker text, blinking markers, fine line drawing Sci-fi games, space tech, cybersecurity, movie props, immersive dashboards Standard corporate, reading heavy content, accessible public services ✓ Low ✓ Full ⚠ Moderate (renders) ⚠ Poor (thin lines) ◐ Medium ✗ Low React 9/10, Canvas 10/10 2010s Sci-Fi High
53 52 Pixel Art General Retro, 8-bit, 16-bit, gaming, blocky, nostalgic, pixelated, arcade Primary colors (NES Palette), brights, limited palette Black outlines, shading via dithering or block colors Frame-by-frame sprite animation, blinking cursor, instant transitions, marquee text Indie games, retro tools, creative portfolios, nostalgia marketing, Web3/NFT Professional corporate, modern SaaS, high-res photography sites ✓ Full ✓ Full ⚡ Excellent ✓ Good (if contrast ok) ✓ High ◐ Medium CSS (box-shadow) 8/10, Canvas 10/10 1980s Arcade Medium
54 53 Bento Grids General Apple-style, modular, cards, organized, clean, hierarchy, grid, rounded, soft Off-white #F5F5F7, Clean White #FFFFFF, Text #1D1D1F Subtle accents, soft shadows, blurred backdrops Hover scale (1.02), soft shadow expansion, smooth layout shifts, content reveal Product features, dashboards, personal sites, marketing summaries, galleries Long-form reading, data tables, complex forms ✓ Full ✓ Full ⚡ Excellent ✓ WCAG AA ✓ High ✓ High CSS Grid 10/10, Tailwind 10/10 2020s Apple/Linear Low
55 54 Neubrutalism General Bold, ugly-cute, raw, high contrast, flat, hard shadows, distinct, playful, loud Pop Yellow #FFDE59, Bright Red #FF5757, Black #000000 Lavender #CBA6F7, Mint #76E0C2 Hard hover shifts (4px), marquee scrolling, jitter animations, bold borders Design tools, creative agencies, Gen Z brands, personal blogs, gumroad-style Banking, legal, healthcare, serious enterprise, elderly users ✓ Full ✓ Full ⚡ Excellent ✓ WCAG AAA ✓ High ✓ High Tailwind 10/10, Plain CSS 10/10 2020s Modern Retro Low
56 55 Spatial UI (VisionOS) General Glass, depth, immersion, spatial, translucent, gaze, gesture, apple, vision-pro Frosted Glass #FFFFFF (15-30% opacity), System White Vibrant system colors for active states, deep shadows for depth Parallax depth, dynamic lighting response, gaze-hover effects, smooth scale on focus Spatial computing apps, VR/AR interfaces, immersive media, futuristic dashboards Text-heavy documents, high-contrast requirements, non-3D capable devices ✓ Full ✓ Full ⚠ Moderate (blur cost) ⚠ Contrast risks ✓ High (if adapted) ✓ High SwiftUI, React (Three.js/Fiber) 2024 Spatial Era High
57 56 E-Ink / Paper General Paper-like, matte, high contrast, texture, reading, calm, slow tech, monochrome Off-White #FDFBF7, Paper White #F5F5F5, Ink Black #1A1A1A Pencil Grey #4A4A4A, Highlighter Yellow #FFFF00 (accent) No motion blur, distinct page turns, grain/noise texture, sharp transitions (no fade) Reading apps, digital newspapers, minimal journals, distraction-free writing, slow-living brands Gaming, video platforms, high-energy marketing, dark mode dependent apps ✓ Full ✗ Low (inverted only) ⚡ Excellent ✓ WCAG AAA ✓ High ✓ Medium Tailwind 10/10, CSS 10/10 2020s Digital Well-being Low
58 57 Gen Z Chaos / Maximalism General Chaos, clutter, stickers, raw, collage, mixed media, loud, internet culture, ironic Clashing Brights: #FF00FF, #00FF00, #FFFF00, #0000FF Gradients, rainbow, glitch, noise, heavily saturated mix Marquee scrolls, jitter, sticker layering, GIF overload, random placement, drag-and-drop Gen Z lifestyle brands, music artists, creative portfolios, viral marketing, fashion Corporate, government, healthcare, banking, serious tools ✓ Full ✓ Full ⚠ Poor (heavy assets) ❌ Poor ◐ Medium ✓ High (Viral) CSS-in-JS 8/10 2023+ Internet Core High
59 58 Biomimetic / Organic 2.0 General Nature-inspired, cellular, fluid, breathing, generative, algorithms, life-like Cellular Pink #FF9999, Chlorophyll Green #00FF41, Bioluminescent Blue Deep Ocean #001E3C, Coral #FF7F50, Organic gradients Breathing animations, fluid morphing, generative growth, physics-based movement Sustainability tech, biotech, advanced health, meditation, generative art platforms Standard SaaS, data grids, strict corporate, accounting ✓ Full ✓ Full ⚠ Moderate ✓ Good ✓ Good ✓ High Canvas 10/10, WebGL 10/10 2024+ Generative High

View File

@@ -1,100 +1,100 @@
No,Category,Issue,Platform,Description,Do,Don't,Code Example Good,Code Example Bad,Severity No,Category,Issue,Platform,Description,Do,Don't,Code Example Good,Code Example Bad,Severity
1,Navigation,Smooth Scroll,Web,Anchor links should scroll smoothly to target section,Use scroll-behavior: smooth on html element,Jump directly without transition,html { scroll-behavior: smooth; },<a href='#section'> without CSS,High 1,Navigation,Smooth Scroll,Web,Anchor links should scroll smoothly to target section,Use scroll-behavior: smooth on html element,Jump directly without transition,html { scroll-behavior: smooth; },<a href='#section'> without CSS,High
2,Navigation,Sticky Navigation,Web,Fixed nav should not obscure content,Add padding-top to body equal to nav height,Let nav overlap first section content,pt-20 (if nav is h-20),No padding compensation,Medium 2,Navigation,Sticky Navigation,Web,Fixed nav should not obscure content,Add padding-top to body equal to nav height,Let nav overlap first section content,pt-20 (if nav is h-20),No padding compensation,Medium
3,Navigation,Active State,All,Current page/section should be visually indicated,Highlight active nav item with color/underline,No visual feedback on current location,text-primary border-b-2,All links same style,Medium 3,Navigation,Active State,All,Current page/section should be visually indicated,Highlight active nav item with color/underline,No visual feedback on current location,text-primary border-b-2,All links same style,Medium
4,Navigation,Back Button,Mobile,Users expect back to work predictably,Preserve navigation history properly,Break browser/app back button behavior,history.pushState(),location.replace(),High 4,Navigation,Back Button,Mobile,Users expect back to work predictably,Preserve navigation history properly,Break browser/app back button behavior,history.pushState(),location.replace(),High
5,Navigation,Deep Linking,All,URLs should reflect current state for sharing,Update URL on state/view changes,Static URLs for dynamic content,Use query params or hash,Single URL for all states,Medium 5,Navigation,Deep Linking,All,URLs should reflect current state for sharing,Update URL on state/view changes,Static URLs for dynamic content,Use query params or hash,Single URL for all states,Medium
6,Navigation,Breadcrumbs,Web,Show user location in site hierarchy,Use for sites with 3+ levels of depth,Use for flat single-level sites,Home > Category > Product,Only on deep nested pages,Low 6,Navigation,Breadcrumbs,Web,Show user location in site hierarchy,Use for sites with 3+ levels of depth,Use for flat single-level sites,Home > Category > Product,Only on deep nested pages,Low
7,Animation,Excessive Motion,All,Too many animations cause distraction and motion sickness,Animate 1-2 key elements per view maximum,Animate everything that moves,Single hero animation,animate-bounce on 5+ elements,High 7,Animation,Excessive Motion,All,Too many animations cause distraction and motion sickness,Animate 1-2 key elements per view maximum,Animate everything that moves,Single hero animation,animate-bounce on 5+ elements,High
8,Animation,Duration Timing,All,Animations should feel responsive not sluggish,Use 150-300ms for micro-interactions,Use animations longer than 500ms for UI,transition-all duration-200,duration-1000,Medium 8,Animation,Duration Timing,All,Animations should feel responsive not sluggish,Use 150-300ms for micro-interactions,Use animations longer than 500ms for UI,transition-all duration-200,duration-1000,Medium
9,Animation,Reduced Motion,All,Respect user's motion preferences,Check prefers-reduced-motion media query,Ignore accessibility motion settings,@media (prefers-reduced-motion: reduce),No motion query check,High 9,Animation,Reduced Motion,All,Respect user's motion preferences,Check prefers-reduced-motion media query,Ignore accessibility motion settings,@media (prefers-reduced-motion: reduce),No motion query check,High
10,Animation,Loading States,All,Show feedback during async operations,Use skeleton screens or spinners,Leave UI frozen with no feedback,animate-pulse skeleton,Blank screen while loading,High 10,Animation,Loading States,All,Show feedback during async operations,Use skeleton screens or spinners,Leave UI frozen with no feedback,animate-pulse skeleton,Blank screen while loading,High
11,Animation,Hover vs Tap,All,Hover effects don't work on touch devices,Use click/tap for primary interactions,Rely only on hover for important actions,onClick handler,onMouseEnter only,High 11,Animation,Hover vs Tap,All,Hover effects don't work on touch devices,Use click/tap for primary interactions,Rely only on hover for important actions,onClick handler,onMouseEnter only,High
12,Animation,Continuous Animation,All,Infinite animations are distracting,Use for loading indicators only,Use for decorative elements,animate-spin on loader,animate-bounce on icons,Medium 12,Animation,Continuous Animation,All,Infinite animations are distracting,Use for loading indicators only,Use for decorative elements,animate-spin on loader,animate-bounce on icons,Medium
13,Animation,Transform Performance,Web,Some CSS properties trigger expensive repaints,Use transform and opacity for animations,Animate width/height/top/left properties,transform: translateY(),top: 10px animation,Medium 13,Animation,Transform Performance,Web,Some CSS properties trigger expensive repaints,Use transform and opacity for animations,Animate width/height/top/left properties,transform: translateY(),top: 10px animation,Medium
14,Animation,Easing Functions,All,Linear motion feels robotic,Use ease-out for entering ease-in for exiting,Use linear for UI transitions,ease-out,linear,Low 14,Animation,Easing Functions,All,Linear motion feels robotic,Use ease-out for entering ease-in for exiting,Use linear for UI transitions,ease-out,linear,Low
15,Layout,Z-Index Management,Web,Stacking context conflicts cause hidden elements,Define z-index scale system (10 20 30 50),Use arbitrary large z-index values,z-10 z-20 z-50,z-[9999],High 15,Layout,Z-Index Management,Web,Stacking context conflicts cause hidden elements,Define z-index scale system (10 20 30 50),Use arbitrary large z-index values,z-10 z-20 z-50,z-[9999],High
16,Layout,Overflow Hidden,Web,Hidden overflow can clip important content,Test all content fits within containers,Blindly apply overflow-hidden,overflow-auto with scroll,overflow-hidden truncating content,Medium 16,Layout,Overflow Hidden,Web,Hidden overflow can clip important content,Test all content fits within containers,Blindly apply overflow-hidden,overflow-auto with scroll,overflow-hidden truncating content,Medium
17,Layout,Fixed Positioning,Web,Fixed elements can overlap or be inaccessible,Account for safe areas and other fixed elements,Stack multiple fixed elements carelessly,Fixed nav + fixed bottom with gap,Multiple overlapping fixed elements,Medium 17,Layout,Fixed Positioning,Web,Fixed elements can overlap or be inaccessible,Account for safe areas and other fixed elements,Stack multiple fixed elements carelessly,Fixed nav + fixed bottom with gap,Multiple overlapping fixed elements,Medium
18,Layout,Stacking Context,Web,New stacking contexts reset z-index,Understand what creates new stacking context,Expect z-index to work across contexts,Parent with z-index isolates children,z-index: 9999 not working,Medium 18,Layout,Stacking Context,Web,New stacking contexts reset z-index,Understand what creates new stacking context,Expect z-index to work across contexts,Parent with z-index isolates children,z-index: 9999 not working,Medium
19,Layout,Content Jumping,Web,Layout shift when content loads is jarring,Reserve space for async content,Let images/content push layout around,aspect-ratio or fixed height,No dimensions on images,High 19,Layout,Content Jumping,Web,Layout shift when content loads is jarring,Reserve space for async content,Let images/content push layout around,aspect-ratio or fixed height,No dimensions on images,High
20,Layout,Viewport Units,Web,100vh can be problematic on mobile browsers,Use dvh or account for mobile browser chrome,Use 100vh for full-screen mobile layouts,min-h-dvh or min-h-screen,h-screen on mobile,Medium 20,Layout,Viewport Units,Web,100vh can be problematic on mobile browsers,Use dvh or account for mobile browser chrome,Use 100vh for full-screen mobile layouts,min-h-dvh or min-h-screen,h-screen on mobile,Medium
21,Layout,Container Width,Web,Content too wide is hard to read,Limit max-width for text content (65-75ch),Let text span full viewport width,max-w-prose or max-w-3xl,Full width paragraphs,Medium 21,Layout,Container Width,Web,Content too wide is hard to read,Limit max-width for text content (65-75ch),Let text span full viewport width,max-w-prose or max-w-3xl,Full width paragraphs,Medium
22,Touch,Touch Target Size,Mobile,Small buttons are hard to tap accurately,Minimum 44x44px touch targets,Tiny clickable areas,min-h-[44px] min-w-[44px],w-6 h-6 buttons,High 22,Touch,Touch Target Size,Mobile,Small buttons are hard to tap accurately,Minimum 44x44px touch targets,Tiny clickable areas,min-h-[44px] min-w-[44px],w-6 h-6 buttons,High
23,Touch,Touch Spacing,Mobile,Adjacent touch targets need adequate spacing,Minimum 8px gap between touch targets,Tightly packed clickable elements,gap-2 between buttons,gap-0 or gap-1,Medium 23,Touch,Touch Spacing,Mobile,Adjacent touch targets need adequate spacing,Minimum 8px gap between touch targets,Tightly packed clickable elements,gap-2 between buttons,gap-0 or gap-1,Medium
24,Touch,Gesture Conflicts,Mobile,Custom gestures can conflict with system,Avoid horizontal swipe on main content,Override system gestures,Vertical scroll primary,Horizontal swipe carousel only,Medium 24,Touch,Gesture Conflicts,Mobile,Custom gestures can conflict with system,Avoid horizontal swipe on main content,Override system gestures,Vertical scroll primary,Horizontal swipe carousel only,Medium
25,Touch,Tap Delay,Mobile,300ms tap delay feels laggy,Use touch-action CSS or fastclick,Default mobile tap handling,touch-action: manipulation,No touch optimization,Medium 25,Touch,Tap Delay,Mobile,300ms tap delay feels laggy,Use touch-action CSS or fastclick,Default mobile tap handling,touch-action: manipulation,No touch optimization,Medium
26,Touch,Pull to Refresh,Mobile,Accidental refresh is frustrating,Disable where not needed,Enable by default everywhere,overscroll-behavior: contain,Default overscroll,Low 26,Touch,Pull to Refresh,Mobile,Accidental refresh is frustrating,Disable where not needed,Enable by default everywhere,overscroll-behavior: contain,Default overscroll,Low
27,Touch,Haptic Feedback,Mobile,Tactile feedback improves interaction feel,Use for confirmations and important actions,Overuse vibration feedback,navigator.vibrate(10),Vibrate on every tap,Low 27,Touch,Haptic Feedback,Mobile,Tactile feedback improves interaction feel,Use for confirmations and important actions,Overuse vibration feedback,navigator.vibrate(10),Vibrate on every tap,Low
28,Interaction,Focus States,All,Keyboard users need visible focus indicators,Use visible focus rings on interactive elements,Remove focus outline without replacement,focus:ring-2 focus:ring-blue-500,outline-none without alternative,High 28,Interaction,Focus States,All,Keyboard users need visible focus indicators,Use visible focus rings on interactive elements,Remove focus outline without replacement,focus:ring-2 focus:ring-blue-500,outline-none without alternative,High
29,Interaction,Hover States,Web,Visual feedback on interactive elements,Change cursor and add subtle visual change,No hover feedback on clickable elements,hover:bg-gray-100 cursor-pointer,No hover style,Medium 29,Interaction,Hover States,Web,Visual feedback on interactive elements,Change cursor and add subtle visual change,No hover feedback on clickable elements,hover:bg-gray-100 cursor-pointer,No hover style,Medium
30,Interaction,Active States,All,Show immediate feedback on press/click,Add pressed/active state visual change,No feedback during interaction,active:scale-95,No active state,Medium 30,Interaction,Active States,All,Show immediate feedback on press/click,Add pressed/active state visual change,No feedback during interaction,active:scale-95,No active state,Medium
31,Interaction,Disabled States,All,Clearly indicate non-interactive elements,Reduce opacity and change cursor,Confuse disabled with normal state,opacity-50 cursor-not-allowed,Same style as enabled,Medium 31,Interaction,Disabled States,All,Clearly indicate non-interactive elements,Reduce opacity and change cursor,Confuse disabled with normal state,opacity-50 cursor-not-allowed,Same style as enabled,Medium
32,Interaction,Loading Buttons,All,Prevent double submission during async actions,Disable button and show loading state,Allow multiple clicks during processing,disabled={loading} spinner,Button clickable while loading,High 32,Interaction,Loading Buttons,All,Prevent double submission during async actions,Disable button and show loading state,Allow multiple clicks during processing,disabled={loading} spinner,Button clickable while loading,High
33,Interaction,Error Feedback,All,Users need to know when something fails,Show clear error messages near problem,Silent failures with no feedback,Red border + error message,No indication of error,High 33,Interaction,Error Feedback,All,Users need to know when something fails,Show clear error messages near problem,Silent failures with no feedback,Red border + error message,No indication of error,High
34,Interaction,Success Feedback,All,Confirm successful actions to users,Show success message or visual change,No confirmation of completed action,Toast notification or checkmark,Action completes silently,Medium 34,Interaction,Success Feedback,All,Confirm successful actions to users,Show success message or visual change,No confirmation of completed action,Toast notification or checkmark,Action completes silently,Medium
35,Interaction,Confirmation Dialogs,All,Prevent accidental destructive actions,Confirm before delete/irreversible actions,Delete without confirmation,Are you sure modal,Direct delete on click,High 35,Interaction,Confirmation Dialogs,All,Prevent accidental destructive actions,Confirm before delete/irreversible actions,Delete without confirmation,Are you sure modal,Direct delete on click,High
36,Accessibility,Color Contrast,All,Text must be readable against background,Minimum 4.5:1 ratio for normal text,Low contrast text,#333 on white (7:1),#999 on white (2.8:1),High 36,Accessibility,Color Contrast,All,Text must be readable against background,Minimum 4.5:1 ratio for normal text,Low contrast text,#333 on white (7:1),#999 on white (2.8:1),High
37,Accessibility,Color Only,All,Don't convey information by color alone,Use icons/text in addition to color,Red/green only for error/success,Red text + error icon,Red border only for error,High 37,Accessibility,Color Only,All,Don't convey information by color alone,Use icons/text in addition to color,Red/green only for error/success,Red text + error icon,Red border only for error,High
38,Accessibility,Alt Text,All,Images need text alternatives,Descriptive alt text for meaningful images,Empty or missing alt attributes,alt='Dog playing in park',alt='' for content images,High 38,Accessibility,Alt Text,All,Images need text alternatives,Descriptive alt text for meaningful images,Empty or missing alt attributes,alt='Dog playing in park',alt='' for content images,High
39,Accessibility,Heading Hierarchy,Web,Screen readers use headings for navigation,Use sequential heading levels h1-h6,Skip heading levels or misuse for styling,h1 then h2 then h3,h1 then h4,Medium 39,Accessibility,Heading Hierarchy,Web,Screen readers use headings for navigation,Use sequential heading levels h1-h6,Skip heading levels or misuse for styling,h1 then h2 then h3,h1 then h4,Medium
40,Accessibility,ARIA Labels,All,Interactive elements need accessible names,Add aria-label for icon-only buttons,Icon buttons without labels,aria-label='Close menu',<button><Icon/></button>,High 40,Accessibility,ARIA Labels,All,Interactive elements need accessible names,Add aria-label for icon-only buttons,Icon buttons without labels,aria-label='Close menu',<button><Icon/></button>,High
41,Accessibility,Keyboard Navigation,Web,All functionality accessible via keyboard,Tab order matches visual order,Keyboard traps or illogical tab order,tabIndex for custom order,Unreachable elements,High 41,Accessibility,Keyboard Navigation,Web,All functionality accessible via keyboard,Tab order matches visual order,Keyboard traps or illogical tab order,tabIndex for custom order,Unreachable elements,High
42,Accessibility,Screen Reader,All,Content should make sense when read aloud,Use semantic HTML and ARIA properly,Div soup with no semantics,<nav> <main> <article>,<div> for everything,Medium 42,Accessibility,Screen Reader,All,Content should make sense when read aloud,Use semantic HTML and ARIA properly,Div soup with no semantics,<nav> <main> <article>,<div> for everything,Medium
43,Accessibility,Form Labels,All,Inputs must have associated labels,Use label with for attribute or wrap input,Placeholder-only inputs,<label for='email'>,placeholder='Email' only,High 43,Accessibility,Form Labels,All,Inputs must have associated labels,Use label with for attribute or wrap input,Placeholder-only inputs,<label for='email'>,placeholder='Email' only,High
44,Accessibility,Error Messages,All,Error messages must be announced,Use aria-live or role=alert for errors,Visual-only error indication,role='alert',Red border only,High 44,Accessibility,Error Messages,All,Error messages must be announced,Use aria-live or role=alert for errors,Visual-only error indication,role='alert',Red border only,High
45,Accessibility,Skip Links,Web,Allow keyboard users to skip navigation,Provide skip to main content link,No skip link on nav-heavy pages,Skip to main content link,100 tabs to reach content,Medium 45,Accessibility,Skip Links,Web,Allow keyboard users to skip navigation,Provide skip to main content link,No skip link on nav-heavy pages,Skip to main content link,100 tabs to reach content,Medium
46,Performance,Image Optimization,All,Large images slow page load,Use appropriate size and format (WebP),Unoptimized full-size images,srcset with multiple sizes,4000px image for 400px display,High 46,Performance,Image Optimization,All,Large images slow page load,Use appropriate size and format (WebP),Unoptimized full-size images,srcset with multiple sizes,4000px image for 400px display,High
47,Performance,Lazy Loading,All,Load content as needed,Lazy load below-fold images and content,Load everything upfront,loading='lazy',All images eager load,Medium 47,Performance,Lazy Loading,All,Load content as needed,Lazy load below-fold images and content,Load everything upfront,loading='lazy',All images eager load,Medium
48,Performance,Code Splitting,Web,Large bundles slow initial load,Split code by route/feature,Single large bundle,dynamic import(),All code in main bundle,Medium 48,Performance,Code Splitting,Web,Large bundles slow initial load,Split code by route/feature,Single large bundle,dynamic import(),All code in main bundle,Medium
49,Performance,Caching,Web,Repeat visits should be fast,Set appropriate cache headers,No caching strategy,Cache-Control headers,Every request hits server,Medium 49,Performance,Caching,Web,Repeat visits should be fast,Set appropriate cache headers,No caching strategy,Cache-Control headers,Every request hits server,Medium
50,Performance,Font Loading,Web,Web fonts can block rendering,Use font-display swap or optional,Invisible text during font load,font-display: swap,FOIT (Flash of Invisible Text),Medium 50,Performance,Font Loading,Web,Web fonts can block rendering,Use font-display swap or optional,Invisible text during font load,font-display: swap,FOIT (Flash of Invisible Text),Medium
51,Performance,Third Party Scripts,Web,External scripts can block rendering,Load non-critical scripts async/defer,Synchronous third-party scripts,async or defer attribute,<script src='...'> in head,Medium 51,Performance,Third Party Scripts,Web,External scripts can block rendering,Load non-critical scripts async/defer,Synchronous third-party scripts,async or defer attribute,<script src='...'> in head,Medium
52,Performance,Bundle Size,Web,Large JavaScript slows interaction,Monitor and minimize bundle size,Ignore bundle size growth,Bundle analyzer,No size monitoring,Medium 52,Performance,Bundle Size,Web,Large JavaScript slows interaction,Monitor and minimize bundle size,Ignore bundle size growth,Bundle analyzer,No size monitoring,Medium
53,Performance,Render Blocking,Web,CSS/JS can block first paint,Inline critical CSS defer non-critical,Large blocking CSS files,Critical CSS inline,All CSS in head,Medium 53,Performance,Render Blocking,Web,CSS/JS can block first paint,Inline critical CSS defer non-critical,Large blocking CSS files,Critical CSS inline,All CSS in head,Medium
54,Forms,Input Labels,All,Every input needs a visible label,Always show label above or beside input,Placeholder as only label,<label>Email</label><input>,placeholder='Email' only,High 54,Forms,Input Labels,All,Every input needs a visible label,Always show label above or beside input,Placeholder as only label,<label>Email</label><input>,placeholder='Email' only,High
55,Forms,Error Placement,All,Errors should appear near the problem,Show error below related input,Single error message at top of form,Error under each field,All errors at form top,Medium 55,Forms,Error Placement,All,Errors should appear near the problem,Show error below related input,Single error message at top of form,Error under each field,All errors at form top,Medium
56,Forms,Inline Validation,All,Validate as user types or on blur,Validate on blur for most fields,Validate only on submit,onBlur validation,Submit-only validation,Medium 56,Forms,Inline Validation,All,Validate as user types or on blur,Validate on blur for most fields,Validate only on submit,onBlur validation,Submit-only validation,Medium
57,Forms,Input Types,All,Use appropriate input types,Use email tel number url etc,Text input for everything,type='email',type='text' for email,Medium 57,Forms,Input Types,All,Use appropriate input types,Use email tel number url etc,Text input for everything,type='email',type='text' for email,Medium
58,Forms,Autofill Support,Web,Help browsers autofill correctly,Use autocomplete attribute properly,Block or ignore autofill,autocomplete='email',autocomplete='off' everywhere,Medium 58,Forms,Autofill Support,Web,Help browsers autofill correctly,Use autocomplete attribute properly,Block or ignore autofill,autocomplete='email',autocomplete='off' everywhere,Medium
59,Forms,Required Indicators,All,Mark required fields clearly,Use asterisk or (required) text,No indication of required fields,* required indicator,Guess which are required,Medium 59,Forms,Required Indicators,All,Mark required fields clearly,Use asterisk or (required) text,No indication of required fields,* required indicator,Guess which are required,Medium
60,Forms,Password Visibility,All,Let users see password while typing,Toggle to show/hide password,No visibility toggle,Show/hide password button,Password always hidden,Medium 60,Forms,Password Visibility,All,Let users see password while typing,Toggle to show/hide password,No visibility toggle,Show/hide password button,Password always hidden,Medium
61,Forms,Submit Feedback,All,Confirm form submission status,Show loading then success/error state,No feedback after submit,Loading -> Success message,Button click with no response,High 61,Forms,Submit Feedback,All,Confirm form submission status,Show loading then success/error state,No feedback after submit,Loading -> Success message,Button click with no response,High
62,Forms,Input Affordance,All,Inputs should look interactive,Use distinct input styling,Inputs that look like plain text,Border/background on inputs,Borderless inputs,Medium 62,Forms,Input Affordance,All,Inputs should look interactive,Use distinct input styling,Inputs that look like plain text,Border/background on inputs,Borderless inputs,Medium
63,Forms,Mobile Keyboards,Mobile,Show appropriate keyboard for input type,Use inputmode attribute,Default keyboard for all inputs,inputmode='numeric',Text keyboard for numbers,Medium 63,Forms,Mobile Keyboards,Mobile,Show appropriate keyboard for input type,Use inputmode attribute,Default keyboard for all inputs,inputmode='numeric',Text keyboard for numbers,Medium
64,Responsive,Mobile First,Web,Design for mobile then enhance for larger,Start with mobile styles then add breakpoints,Desktop-first causing mobile issues,Default mobile + md: lg: xl:,Desktop default + max-width queries,Medium 64,Responsive,Mobile First,Web,Design for mobile then enhance for larger,Start with mobile styles then add breakpoints,Desktop-first causing mobile issues,Default mobile + md: lg: xl:,Desktop default + max-width queries,Medium
65,Responsive,Breakpoint Testing,Web,Test at all common screen sizes,Test at 320 375 414 768 1024 1440,Only test on your device,Multiple device testing,Single device development,Medium 65,Responsive,Breakpoint Testing,Web,Test at all common screen sizes,Test at 320 375 414 768 1024 1440,Only test on your device,Multiple device testing,Single device development,Medium
66,Responsive,Touch Friendly,Web,Mobile layouts need touch-sized targets,Increase touch targets on mobile,Same tiny buttons on mobile,Larger buttons on mobile,Desktop-sized targets on mobile,High 66,Responsive,Touch Friendly,Web,Mobile layouts need touch-sized targets,Increase touch targets on mobile,Same tiny buttons on mobile,Larger buttons on mobile,Desktop-sized targets on mobile,High
67,Responsive,Readable Font Size,All,Text must be readable on all devices,Minimum 16px body text on mobile,Tiny text on mobile,text-base or larger,text-xs for body text,High 67,Responsive,Readable Font Size,All,Text must be readable on all devices,Minimum 16px body text on mobile,Tiny text on mobile,text-base or larger,text-xs for body text,High
68,Responsive,Viewport Meta,Web,Set viewport for mobile devices,Use width=device-width initial-scale=1,Missing or incorrect viewport,<meta name='viewport'...>,No viewport meta tag,High 68,Responsive,Viewport Meta,Web,Set viewport for mobile devices,Use width=device-width initial-scale=1,Missing or incorrect viewport,<meta name='viewport'...>,No viewport meta tag,High
69,Responsive,Horizontal Scroll,Web,Avoid horizontal scrolling,Ensure content fits viewport width,Content wider than viewport,max-w-full overflow-x-hidden,Horizontal scrollbar on mobile,High 69,Responsive,Horizontal Scroll,Web,Avoid horizontal scrolling,Ensure content fits viewport width,Content wider than viewport,max-w-full overflow-x-hidden,Horizontal scrollbar on mobile,High
70,Responsive,Image Scaling,Web,Images should scale with container,Use max-width: 100% on images,Fixed width images overflow,max-w-full h-auto,width='800' fixed,Medium 70,Responsive,Image Scaling,Web,Images should scale with container,Use max-width: 100% on images,Fixed width images overflow,max-w-full h-auto,width='800' fixed,Medium
71,Responsive,Table Handling,Web,Tables can overflow on mobile,Use horizontal scroll or card layout,Wide tables breaking layout,overflow-x-auto wrapper,Table overflows viewport,Medium 71,Responsive,Table Handling,Web,Tables can overflow on mobile,Use horizontal scroll or card layout,Wide tables breaking layout,overflow-x-auto wrapper,Table overflows viewport,Medium
72,Typography,Line Height,All,Adequate line height improves readability,Use 1.5-1.75 for body text,Cramped or excessive line height,leading-relaxed (1.625),leading-none (1),Medium 72,Typography,Line Height,All,Adequate line height improves readability,Use 1.5-1.75 for body text,Cramped or excessive line height,leading-relaxed (1.625),leading-none (1),Medium
73,Typography,Line Length,Web,Long lines are hard to read,Limit to 65-75 characters per line,Full-width text on large screens,max-w-prose,Full viewport width text,Medium 73,Typography,Line Length,Web,Long lines are hard to read,Limit to 65-75 characters per line,Full-width text on large screens,max-w-prose,Full viewport width text,Medium
74,Typography,Font Size Scale,All,Consistent type hierarchy aids scanning,Use consistent modular scale,Random font sizes,Type scale (12 14 16 18 24 32),Arbitrary sizes,Medium 74,Typography,Font Size Scale,All,Consistent type hierarchy aids scanning,Use consistent modular scale,Random font sizes,Type scale (12 14 16 18 24 32),Arbitrary sizes,Medium
75,Typography,Font Loading,Web,Fonts should load without layout shift,Reserve space with fallback font,Layout shift when fonts load,font-display: swap + similar fallback,No fallback font,Medium 75,Typography,Font Loading,Web,Fonts should load without layout shift,Reserve space with fallback font,Layout shift when fonts load,font-display: swap + similar fallback,No fallback font,Medium
76,Typography,Contrast Readability,All,Body text needs good contrast,Use darker text on light backgrounds,Gray text on gray background,text-gray-900 on white,text-gray-400 on gray-100,High 76,Typography,Contrast Readability,All,Body text needs good contrast,Use darker text on light backgrounds,Gray text on gray background,text-gray-900 on white,text-gray-400 on gray-100,High
77,Typography,Heading Clarity,All,Headings should stand out from body,Clear size/weight difference,Headings similar to body text,Bold + larger size,Same size as body,Medium 77,Typography,Heading Clarity,All,Headings should stand out from body,Clear size/weight difference,Headings similar to body text,Bold + larger size,Same size as body,Medium
78,Feedback,Loading Indicators,All,Show system status during waits,Show spinner/skeleton for operations > 300ms,No feedback during loading,Skeleton or spinner,Frozen UI,High 78,Feedback,Loading Indicators,All,Show system status during waits,Show spinner/skeleton for operations > 300ms,No feedback during loading,Skeleton or spinner,Frozen UI,High
79,Feedback,Empty States,All,Guide users when no content exists,Show helpful message and action,Blank empty screens,No items yet. Create one!,Empty white space,Medium 79,Feedback,Empty States,All,Guide users when no content exists,Show helpful message and action,Blank empty screens,No items yet. Create one!,Empty white space,Medium
80,Feedback,Error Recovery,All,Help users recover from errors,Provide clear next steps,Error without recovery path,Try again button + help link,Error message only,Medium 80,Feedback,Error Recovery,All,Help users recover from errors,Provide clear next steps,Error without recovery path,Try again button + help link,Error message only,Medium
81,Feedback,Progress Indicators,All,Show progress for multi-step processes,Step indicators or progress bar,No indication of progress,Step 2 of 4 indicator,No step information,Medium 81,Feedback,Progress Indicators,All,Show progress for multi-step processes,Step indicators or progress bar,No indication of progress,Step 2 of 4 indicator,No step information,Medium
82,Feedback,Toast Notifications,All,Transient messages for non-critical info,Auto-dismiss after 3-5 seconds,Toasts that never disappear,Auto-dismiss toast,Persistent toast,Medium 82,Feedback,Toast Notifications,All,Transient messages for non-critical info,Auto-dismiss after 3-5 seconds,Toasts that never disappear,Auto-dismiss toast,Persistent toast,Medium
83,Feedback,Confirmation Messages,All,Confirm successful actions,Brief success message,Silent success,Saved successfully toast,No confirmation,Medium 83,Feedback,Confirmation Messages,All,Confirm successful actions,Brief success message,Silent success,Saved successfully toast,No confirmation,Medium
84,Content,Truncation,All,Handle long content gracefully,Truncate with ellipsis and expand option,Overflow or broken layout,line-clamp-2 with expand,Overflow or cut off,Medium 84,Content,Truncation,All,Handle long content gracefully,Truncate with ellipsis and expand option,Overflow or broken layout,line-clamp-2 with expand,Overflow or cut off,Medium
85,Content,Date Formatting,All,Use locale-appropriate date formats,Use relative or locale-aware dates,Ambiguous date formats,2 hours ago or locale format,01/02/03,Low 85,Content,Date Formatting,All,Use locale-appropriate date formats,Use relative or locale-aware dates,Ambiguous date formats,2 hours ago or locale format,01/02/03,Low
86,Content,Number Formatting,All,Format large numbers for readability,Use thousand separators or abbreviations,Long unformatted numbers,"1.2K or 1,234",1234567,Low 86,Content,Number Formatting,All,Format large numbers for readability,Use thousand separators or abbreviations,Long unformatted numbers,"1.2K or 1,234",1234567,Low
87,Content,Placeholder Content,All,Show realistic placeholders during dev,Use realistic sample data,Lorem ipsum everywhere,Real sample content,Lorem ipsum,Low 87,Content,Placeholder Content,All,Show realistic placeholders during dev,Use realistic sample data,Lorem ipsum everywhere,Real sample content,Lorem ipsum,Low
88,Onboarding,User Freedom,All,Users should be able to skip tutorials,Provide Skip and Back buttons,Force linear unskippable tour,Skip Tutorial button,Locked overlay until finished,Medium 88,Onboarding,User Freedom,All,Users should be able to skip tutorials,Provide Skip and Back buttons,Force linear unskippable tour,Skip Tutorial button,Locked overlay until finished,Medium
89,Search,Autocomplete,Web,Help users find results faster,Show predictions as user types,Require full type and enter,Debounced fetch + dropdown,No suggestions,Medium 89,Search,Autocomplete,Web,Help users find results faster,Show predictions as user types,Require full type and enter,Debounced fetch + dropdown,No suggestions,Medium
90,Search,No Results,Web,Dead ends frustrate users,Show 'No results' with suggestions,Blank screen or '0 results',Try searching for X instead,No results found.,Medium 90,Search,No Results,Web,Dead ends frustrate users,Show 'No results' with suggestions,Blank screen or '0 results',Try searching for X instead,No results found.,Medium
91,Data Entry,Bulk Actions,Web,Editing one by one is tedious,Allow multi-select and bulk edit,Single row actions only,Checkbox column + Action bar,Repeated actions per row,Low 91,Data Entry,Bulk Actions,Web,Editing one by one is tedious,Allow multi-select and bulk edit,Single row actions only,Checkbox column + Action bar,Repeated actions per row,Low
92,AI Interaction,Disclaimer,All,Users need to know they talk to AI,Clearly label AI generated content,Present AI as human,AI Assistant label,Fake human name without label,High 92,AI Interaction,Disclaimer,All,Users need to know they talk to AI,Clearly label AI generated content,Present AI as human,AI Assistant label,Fake human name without label,High
93,AI Interaction,Streaming,All,Waiting for full text is slow,Stream text response token by token,Show loading spinner for 10s+,Typewriter effect,Spinner until 100% complete,Medium 93,AI Interaction,Streaming,All,Waiting for full text is slow,Stream text response token by token,Show loading spinner for 10s+,Typewriter effect,Spinner until 100% complete,Medium
94,Spatial UI,Gaze Hover,VisionOS,Elements should respond to eye tracking before pinch,Scale/highlight element on look,Static element until pinch,hoverEffect(),onTap only,High 94,Spatial UI,Gaze Hover,VisionOS,Elements should respond to eye tracking before pinch,Scale/highlight element on look,Static element until pinch,hoverEffect(),onTap only,High
95,Spatial UI,Depth Layering,VisionOS,UI needs Z-depth to separate content from environment,Use glass material and z-offset,Flat opaque panels blocking view,.glassBackgroundEffect(),bg-white,Medium 95,Spatial UI,Depth Layering,VisionOS,UI needs Z-depth to separate content from environment,Use glass material and z-offset,Flat opaque panels blocking view,.glassBackgroundEffect(),bg-white,Medium
96,Sustainability,Auto-Play Video,Web,Video consumes massive data and energy,Click-to-play or pause when off-screen,Auto-play high-res video loops,playsInline muted preload='none',autoplay loop,Medium 96,Sustainability,Auto-Play Video,Web,Video consumes massive data and energy,Click-to-play or pause when off-screen,Auto-play high-res video loops,playsInline muted preload='none',autoplay loop,Medium
97,Sustainability,Asset Weight,Web,Heavy 3D/Image assets increase carbon footprint,Compress and lazy load 3D models,Load 50MB textures,Draco compression,Raw .obj files,Medium 97,Sustainability,Asset Weight,Web,Heavy 3D/Image assets increase carbon footprint,Compress and lazy load 3D models,Load 50MB textures,Draco compression,Raw .obj files,Medium
98,AI Interaction,Feedback Loop,All,AI needs user feedback to improve,Thumps up/down or 'Regenerate',Static output only,Feedback component,Read-only text,Low 98,AI Interaction,Feedback Loop,All,AI needs user feedback to improve,Thumps up/down or 'Regenerate',Static output only,Feedback component,Read-only text,Low
99,Accessibility,Motion Sensitivity,All,Parallax/Scroll-jacking causes nausea,Respect prefers-reduced-motion,Force scroll effects,@media (prefers-reduced-motion),ScrollTrigger.create(),High 99,Accessibility,Motion Sensitivity,All,Parallax/Scroll-jacking causes nausea,Respect prefers-reduced-motion,Force scroll effects,@media (prefers-reduced-motion),ScrollTrigger.create(),High
1 No Category Issue Platform Description Do Don't Code Example Good Code Example Bad Severity
2 1 Navigation Smooth Scroll Web Anchor links should scroll smoothly to target section Use scroll-behavior: smooth on html element Jump directly without transition html { scroll-behavior: smooth; } <a href='#section'> without CSS High
3 2 Navigation Sticky Navigation Web Fixed nav should not obscure content Add padding-top to body equal to nav height Let nav overlap first section content pt-20 (if nav is h-20) No padding compensation Medium
4 3 Navigation Active State All Current page/section should be visually indicated Highlight active nav item with color/underline No visual feedback on current location text-primary border-b-2 All links same style Medium
5 4 Navigation Back Button Mobile Users expect back to work predictably Preserve navigation history properly Break browser/app back button behavior history.pushState() location.replace() High
6 5 Navigation Deep Linking All URLs should reflect current state for sharing Update URL on state/view changes Static URLs for dynamic content Use query params or hash Single URL for all states Medium
7 6 Navigation Breadcrumbs Web Show user location in site hierarchy Use for sites with 3+ levels of depth Use for flat single-level sites Home > Category > Product Only on deep nested pages Low
8 7 Animation Excessive Motion All Too many animations cause distraction and motion sickness Animate 1-2 key elements per view maximum Animate everything that moves Single hero animation animate-bounce on 5+ elements High
9 8 Animation Duration Timing All Animations should feel responsive not sluggish Use 150-300ms for micro-interactions Use animations longer than 500ms for UI transition-all duration-200 duration-1000 Medium
10 9 Animation Reduced Motion All Respect user's motion preferences Check prefers-reduced-motion media query Ignore accessibility motion settings @media (prefers-reduced-motion: reduce) No motion query check High
11 10 Animation Loading States All Show feedback during async operations Use skeleton screens or spinners Leave UI frozen with no feedback animate-pulse skeleton Blank screen while loading High
12 11 Animation Hover vs Tap All Hover effects don't work on touch devices Use click/tap for primary interactions Rely only on hover for important actions onClick handler onMouseEnter only High
13 12 Animation Continuous Animation All Infinite animations are distracting Use for loading indicators only Use for decorative elements animate-spin on loader animate-bounce on icons Medium
14 13 Animation Transform Performance Web Some CSS properties trigger expensive repaints Use transform and opacity for animations Animate width/height/top/left properties transform: translateY() top: 10px animation Medium
15 14 Animation Easing Functions All Linear motion feels robotic Use ease-out for entering ease-in for exiting Use linear for UI transitions ease-out linear Low
16 15 Layout Z-Index Management Web Stacking context conflicts cause hidden elements Define z-index scale system (10 20 30 50) Use arbitrary large z-index values z-10 z-20 z-50 z-[9999] High
17 16 Layout Overflow Hidden Web Hidden overflow can clip important content Test all content fits within containers Blindly apply overflow-hidden overflow-auto with scroll overflow-hidden truncating content Medium
18 17 Layout Fixed Positioning Web Fixed elements can overlap or be inaccessible Account for safe areas and other fixed elements Stack multiple fixed elements carelessly Fixed nav + fixed bottom with gap Multiple overlapping fixed elements Medium
19 18 Layout Stacking Context Web New stacking contexts reset z-index Understand what creates new stacking context Expect z-index to work across contexts Parent with z-index isolates children z-index: 9999 not working Medium
20 19 Layout Content Jumping Web Layout shift when content loads is jarring Reserve space for async content Let images/content push layout around aspect-ratio or fixed height No dimensions on images High
21 20 Layout Viewport Units Web 100vh can be problematic on mobile browsers Use dvh or account for mobile browser chrome Use 100vh for full-screen mobile layouts min-h-dvh or min-h-screen h-screen on mobile Medium
22 21 Layout Container Width Web Content too wide is hard to read Limit max-width for text content (65-75ch) Let text span full viewport width max-w-prose or max-w-3xl Full width paragraphs Medium
23 22 Touch Touch Target Size Mobile Small buttons are hard to tap accurately Minimum 44x44px touch targets Tiny clickable areas min-h-[44px] min-w-[44px] w-6 h-6 buttons High
24 23 Touch Touch Spacing Mobile Adjacent touch targets need adequate spacing Minimum 8px gap between touch targets Tightly packed clickable elements gap-2 between buttons gap-0 or gap-1 Medium
25 24 Touch Gesture Conflicts Mobile Custom gestures can conflict with system Avoid horizontal swipe on main content Override system gestures Vertical scroll primary Horizontal swipe carousel only Medium
26 25 Touch Tap Delay Mobile 300ms tap delay feels laggy Use touch-action CSS or fastclick Default mobile tap handling touch-action: manipulation No touch optimization Medium
27 26 Touch Pull to Refresh Mobile Accidental refresh is frustrating Disable where not needed Enable by default everywhere overscroll-behavior: contain Default overscroll Low
28 27 Touch Haptic Feedback Mobile Tactile feedback improves interaction feel Use for confirmations and important actions Overuse vibration feedback navigator.vibrate(10) Vibrate on every tap Low
29 28 Interaction Focus States All Keyboard users need visible focus indicators Use visible focus rings on interactive elements Remove focus outline without replacement focus:ring-2 focus:ring-blue-500 outline-none without alternative High
30 29 Interaction Hover States Web Visual feedback on interactive elements Change cursor and add subtle visual change No hover feedback on clickable elements hover:bg-gray-100 cursor-pointer No hover style Medium
31 30 Interaction Active States All Show immediate feedback on press/click Add pressed/active state visual change No feedback during interaction active:scale-95 No active state Medium
32 31 Interaction Disabled States All Clearly indicate non-interactive elements Reduce opacity and change cursor Confuse disabled with normal state opacity-50 cursor-not-allowed Same style as enabled Medium
33 32 Interaction Loading Buttons All Prevent double submission during async actions Disable button and show loading state Allow multiple clicks during processing disabled={loading} spinner Button clickable while loading High
34 33 Interaction Error Feedback All Users need to know when something fails Show clear error messages near problem Silent failures with no feedback Red border + error message No indication of error High
35 34 Interaction Success Feedback All Confirm successful actions to users Show success message or visual change No confirmation of completed action Toast notification or checkmark Action completes silently Medium
36 35 Interaction Confirmation Dialogs All Prevent accidental destructive actions Confirm before delete/irreversible actions Delete without confirmation Are you sure modal Direct delete on click High
37 36 Accessibility Color Contrast All Text must be readable against background Minimum 4.5:1 ratio for normal text Low contrast text #333 on white (7:1) #999 on white (2.8:1) High
38 37 Accessibility Color Only All Don't convey information by color alone Use icons/text in addition to color Red/green only for error/success Red text + error icon Red border only for error High
39 38 Accessibility Alt Text All Images need text alternatives Descriptive alt text for meaningful images Empty or missing alt attributes alt='Dog playing in park' alt='' for content images High
40 39 Accessibility Heading Hierarchy Web Screen readers use headings for navigation Use sequential heading levels h1-h6 Skip heading levels or misuse for styling h1 then h2 then h3 h1 then h4 Medium
41 40 Accessibility ARIA Labels All Interactive elements need accessible names Add aria-label for icon-only buttons Icon buttons without labels aria-label='Close menu' <button><Icon/></button> High
42 41 Accessibility Keyboard Navigation Web All functionality accessible via keyboard Tab order matches visual order Keyboard traps or illogical tab order tabIndex for custom order Unreachable elements High
43 42 Accessibility Screen Reader All Content should make sense when read aloud Use semantic HTML and ARIA properly Div soup with no semantics <nav> <main> <article> <div> for everything Medium
44 43 Accessibility Form Labels All Inputs must have associated labels Use label with for attribute or wrap input Placeholder-only inputs <label for='email'> placeholder='Email' only High
45 44 Accessibility Error Messages All Error messages must be announced Use aria-live or role=alert for errors Visual-only error indication role='alert' Red border only High
46 45 Accessibility Skip Links Web Allow keyboard users to skip navigation Provide skip to main content link No skip link on nav-heavy pages Skip to main content link 100 tabs to reach content Medium
47 46 Performance Image Optimization All Large images slow page load Use appropriate size and format (WebP) Unoptimized full-size images srcset with multiple sizes 4000px image for 400px display High
48 47 Performance Lazy Loading All Load content as needed Lazy load below-fold images and content Load everything upfront loading='lazy' All images eager load Medium
49 48 Performance Code Splitting Web Large bundles slow initial load Split code by route/feature Single large bundle dynamic import() All code in main bundle Medium
50 49 Performance Caching Web Repeat visits should be fast Set appropriate cache headers No caching strategy Cache-Control headers Every request hits server Medium
51 50 Performance Font Loading Web Web fonts can block rendering Use font-display swap or optional Invisible text during font load font-display: swap FOIT (Flash of Invisible Text) Medium
52 51 Performance Third Party Scripts Web External scripts can block rendering Load non-critical scripts async/defer Synchronous third-party scripts async or defer attribute <script src='...'> in head Medium
53 52 Performance Bundle Size Web Large JavaScript slows interaction Monitor and minimize bundle size Ignore bundle size growth Bundle analyzer No size monitoring Medium
54 53 Performance Render Blocking Web CSS/JS can block first paint Inline critical CSS defer non-critical Large blocking CSS files Critical CSS inline All CSS in head Medium
55 54 Forms Input Labels All Every input needs a visible label Always show label above or beside input Placeholder as only label <label>Email</label><input> placeholder='Email' only High
56 55 Forms Error Placement All Errors should appear near the problem Show error below related input Single error message at top of form Error under each field All errors at form top Medium
57 56 Forms Inline Validation All Validate as user types or on blur Validate on blur for most fields Validate only on submit onBlur validation Submit-only validation Medium
58 57 Forms Input Types All Use appropriate input types Use email tel number url etc Text input for everything type='email' type='text' for email Medium
59 58 Forms Autofill Support Web Help browsers autofill correctly Use autocomplete attribute properly Block or ignore autofill autocomplete='email' autocomplete='off' everywhere Medium
60 59 Forms Required Indicators All Mark required fields clearly Use asterisk or (required) text No indication of required fields * required indicator Guess which are required Medium
61 60 Forms Password Visibility All Let users see password while typing Toggle to show/hide password No visibility toggle Show/hide password button Password always hidden Medium
62 61 Forms Submit Feedback All Confirm form submission status Show loading then success/error state No feedback after submit Loading -> Success message Button click with no response High
63 62 Forms Input Affordance All Inputs should look interactive Use distinct input styling Inputs that look like plain text Border/background on inputs Borderless inputs Medium
64 63 Forms Mobile Keyboards Mobile Show appropriate keyboard for input type Use inputmode attribute Default keyboard for all inputs inputmode='numeric' Text keyboard for numbers Medium
65 64 Responsive Mobile First Web Design for mobile then enhance for larger Start with mobile styles then add breakpoints Desktop-first causing mobile issues Default mobile + md: lg: xl: Desktop default + max-width queries Medium
66 65 Responsive Breakpoint Testing Web Test at all common screen sizes Test at 320 375 414 768 1024 1440 Only test on your device Multiple device testing Single device development Medium
67 66 Responsive Touch Friendly Web Mobile layouts need touch-sized targets Increase touch targets on mobile Same tiny buttons on mobile Larger buttons on mobile Desktop-sized targets on mobile High
68 67 Responsive Readable Font Size All Text must be readable on all devices Minimum 16px body text on mobile Tiny text on mobile text-base or larger text-xs for body text High
69 68 Responsive Viewport Meta Web Set viewport for mobile devices Use width=device-width initial-scale=1 Missing or incorrect viewport <meta name='viewport'...> No viewport meta tag High
70 69 Responsive Horizontal Scroll Web Avoid horizontal scrolling Ensure content fits viewport width Content wider than viewport max-w-full overflow-x-hidden Horizontal scrollbar on mobile High
71 70 Responsive Image Scaling Web Images should scale with container Use max-width: 100% on images Fixed width images overflow max-w-full h-auto width='800' fixed Medium
72 71 Responsive Table Handling Web Tables can overflow on mobile Use horizontal scroll or card layout Wide tables breaking layout overflow-x-auto wrapper Table overflows viewport Medium
73 72 Typography Line Height All Adequate line height improves readability Use 1.5-1.75 for body text Cramped or excessive line height leading-relaxed (1.625) leading-none (1) Medium
74 73 Typography Line Length Web Long lines are hard to read Limit to 65-75 characters per line Full-width text on large screens max-w-prose Full viewport width text Medium
75 74 Typography Font Size Scale All Consistent type hierarchy aids scanning Use consistent modular scale Random font sizes Type scale (12 14 16 18 24 32) Arbitrary sizes Medium
76 75 Typography Font Loading Web Fonts should load without layout shift Reserve space with fallback font Layout shift when fonts load font-display: swap + similar fallback No fallback font Medium
77 76 Typography Contrast Readability All Body text needs good contrast Use darker text on light backgrounds Gray text on gray background text-gray-900 on white text-gray-400 on gray-100 High
78 77 Typography Heading Clarity All Headings should stand out from body Clear size/weight difference Headings similar to body text Bold + larger size Same size as body Medium
79 78 Feedback Loading Indicators All Show system status during waits Show spinner/skeleton for operations > 300ms No feedback during loading Skeleton or spinner Frozen UI High
80 79 Feedback Empty States All Guide users when no content exists Show helpful message and action Blank empty screens No items yet. Create one! Empty white space Medium
81 80 Feedback Error Recovery All Help users recover from errors Provide clear next steps Error without recovery path Try again button + help link Error message only Medium
82 81 Feedback Progress Indicators All Show progress for multi-step processes Step indicators or progress bar No indication of progress Step 2 of 4 indicator No step information Medium
83 82 Feedback Toast Notifications All Transient messages for non-critical info Auto-dismiss after 3-5 seconds Toasts that never disappear Auto-dismiss toast Persistent toast Medium
84 83 Feedback Confirmation Messages All Confirm successful actions Brief success message Silent success Saved successfully toast No confirmation Medium
85 84 Content Truncation All Handle long content gracefully Truncate with ellipsis and expand option Overflow or broken layout line-clamp-2 with expand Overflow or cut off Medium
86 85 Content Date Formatting All Use locale-appropriate date formats Use relative or locale-aware dates Ambiguous date formats 2 hours ago or locale format 01/02/03 Low
87 86 Content Number Formatting All Format large numbers for readability Use thousand separators or abbreviations Long unformatted numbers 1.2K or 1,234 1234567 Low
88 87 Content Placeholder Content All Show realistic placeholders during dev Use realistic sample data Lorem ipsum everywhere Real sample content Lorem ipsum Low
89 88 Onboarding User Freedom All Users should be able to skip tutorials Provide Skip and Back buttons Force linear unskippable tour Skip Tutorial button Locked overlay until finished Medium
90 89 Search Autocomplete Web Help users find results faster Show predictions as user types Require full type and enter Debounced fetch + dropdown No suggestions Medium
91 90 Search No Results Web Dead ends frustrate users Show 'No results' with suggestions Blank screen or '0 results' Try searching for X instead No results found. Medium
92 91 Data Entry Bulk Actions Web Editing one by one is tedious Allow multi-select and bulk edit Single row actions only Checkbox column + Action bar Repeated actions per row Low
93 92 AI Interaction Disclaimer All Users need to know they talk to AI Clearly label AI generated content Present AI as human AI Assistant label Fake human name without label High
94 93 AI Interaction Streaming All Waiting for full text is slow Stream text response token by token Show loading spinner for 10s+ Typewriter effect Spinner until 100% complete Medium
95 94 Spatial UI Gaze Hover VisionOS Elements should respond to eye tracking before pinch Scale/highlight element on look Static element until pinch hoverEffect() onTap only High
96 95 Spatial UI Depth Layering VisionOS UI needs Z-depth to separate content from environment Use glass material and z-offset Flat opaque panels blocking view .glassBackgroundEffect() bg-white Medium
97 96 Sustainability Auto-Play Video Web Video consumes massive data and energy Click-to-play or pause when off-screen Auto-play high-res video loops playsInline muted preload='none' autoplay loop Medium
98 97 Sustainability Asset Weight Web Heavy 3D/Image assets increase carbon footprint Compress and lazy load 3D models Load 50MB textures Draco compression Raw .obj files Medium
99 98 AI Interaction Feedback Loop All AI needs user feedback to improve Thumps up/down or 'Regenerate' Static output only Feedback component Read-only text Low
100 99 Accessibility Motion Sensitivity All Parallax/Scroll-jacking causes nausea Respect prefers-reduced-motion Force scroll effects @media (prefers-reduced-motion) ScrollTrigger.create() High

View File

@@ -1,76 +1,76 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
UI/UX Pro Max Search - BM25 search engine for UI/UX style guides UI/UX Pro Max Search - BM25 search engine for UI/UX style guides
Usage: python search.py "<query>" [--domain <domain>] [--stack <stack>] [--max-results 3] Usage: python search.py "<query>" [--domain <domain>] [--stack <stack>] [--max-results 3]
python search.py "<query>" --design-system [-p "Project Name"] python search.py "<query>" --design-system [-p "Project Name"]
Domains: style, prompt, color, chart, landing, product, ux, typography Domains: style, prompt, color, chart, landing, product, ux, typography
Stacks: html-tailwind, react, nextjs Stacks: html-tailwind, react, nextjs
""" """
import argparse import argparse
from core import CSV_CONFIG, AVAILABLE_STACKS, MAX_RESULTS, search, search_stack from core import CSV_CONFIG, AVAILABLE_STACKS, MAX_RESULTS, search, search_stack
from design_system import generate_design_system from design_system import generate_design_system
def format_output(result): def format_output(result):
"""Format results for Claude consumption (token-optimized)""" """Format results for Claude consumption (token-optimized)"""
if "error" in result: if "error" in result:
return f"Error: {result['error']}" return f"Error: {result['error']}"
output = [] output = []
if result.get("stack"): if result.get("stack"):
output.append(f"## UI Pro Max Stack Guidelines") output.append(f"## UI Pro Max Stack Guidelines")
output.append(f"**Stack:** {result['stack']} | **Query:** {result['query']}") output.append(f"**Stack:** {result['stack']} | **Query:** {result['query']}")
else: else:
output.append(f"## UI Pro Max Search Results") output.append(f"## UI Pro Max Search Results")
output.append(f"**Domain:** {result['domain']} | **Query:** {result['query']}") output.append(f"**Domain:** {result['domain']} | **Query:** {result['query']}")
output.append(f"**Source:** {result['file']} | **Found:** {result['count']} results\n") output.append(f"**Source:** {result['file']} | **Found:** {result['count']} results\n")
for i, row in enumerate(result['results'], 1): for i, row in enumerate(result['results'], 1):
output.append(f"### Result {i}") output.append(f"### Result {i}")
for key, value in row.items(): for key, value in row.items():
value_str = str(value) value_str = str(value)
if len(value_str) > 300: if len(value_str) > 300:
value_str = value_str[:300] + "..." value_str = value_str[:300] + "..."
output.append(f"- **{key}:** {value_str}") output.append(f"- **{key}:** {value_str}")
output.append("") output.append("")
return "\n".join(output) return "\n".join(output)
if __name__ == "__main__": if __name__ == "__main__":
parser = argparse.ArgumentParser(description="UI Pro Max Search") parser = argparse.ArgumentParser(description="UI Pro Max Search")
parser.add_argument("query", help="Search query") parser.add_argument("query", help="Search query")
parser.add_argument("--domain", "-d", choices=list(CSV_CONFIG.keys()), help="Search domain") parser.add_argument("--domain", "-d", choices=list(CSV_CONFIG.keys()), help="Search domain")
parser.add_argument("--stack", "-s", choices=AVAILABLE_STACKS, help="Stack-specific search (html-tailwind, react, nextjs)") parser.add_argument("--stack", "-s", choices=AVAILABLE_STACKS, help="Stack-specific search (html-tailwind, react, nextjs)")
parser.add_argument("--max-results", "-n", type=int, default=MAX_RESULTS, help="Max results (default: 3)") parser.add_argument("--max-results", "-n", type=int, default=MAX_RESULTS, help="Max results (default: 3)")
parser.add_argument("--json", action="store_true", help="Output as JSON") parser.add_argument("--json", action="store_true", help="Output as JSON")
# Design system generation # Design system generation
parser.add_argument("--design-system", "-ds", action="store_true", help="Generate complete design system recommendation") parser.add_argument("--design-system", "-ds", action="store_true", help="Generate complete design system recommendation")
parser.add_argument("--project-name", "-p", type=str, default=None, help="Project name for design system output") parser.add_argument("--project-name", "-p", type=str, default=None, help="Project name for design system output")
parser.add_argument("--format", "-f", choices=["ascii", "markdown"], default="ascii", help="Output format for design system") parser.add_argument("--format", "-f", choices=["ascii", "markdown"], default="ascii", help="Output format for design system")
args = parser.parse_args() args = parser.parse_args()
# Design system takes priority # Design system takes priority
if args.design_system: if args.design_system:
result = generate_design_system(args.query, args.project_name, args.format) result = generate_design_system(args.query, args.project_name, args.format)
print(result) print(result)
# Stack search # Stack search
elif args.stack: elif args.stack:
result = search_stack(args.query, args.stack, args.max_results) result = search_stack(args.query, args.stack, args.max_results)
if args.json: if args.json:
import json import json
print(json.dumps(result, indent=2, ensure_ascii=False)) print(json.dumps(result, indent=2, ensure_ascii=False))
else: else:
print(format_output(result)) print(format_output(result))
# Domain search # Domain search
else: else:
result = search(args.query, args.domain, args.max_results) result = search(args.query, args.domain, args.max_results)
if args.json: if args.json:
import json import json
print(json.dumps(result, indent=2, ensure_ascii=False)) print(json.dumps(result, indent=2, ensure_ascii=False))
else: else:
print(format_output(result)) print(format_output(result))

36
LICENSE
View File

@@ -1,18 +1,18 @@
MIT License MIT License
Copyright (c) 2026 XuJiacheng Copyright (c) 2026 XuJiacheng
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
associated documentation files (the "Software"), to deal in the Software without restriction, including associated documentation files (the "Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
following conditions: following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial The above copyright notice and this permission notice shall be included in all copies or substantial
portions of the Software. portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
USE OR OTHER DEALINGS IN THE SOFTWARE. USE OR OTHER DEALINGS IN THE SOFTWARE.

327
README.md
View File

@@ -1,174 +1,153 @@
# BrowserBookmark # BrowserBookmark
内部私域书签管理Web + 浏览器扩展MV3 内部私域书签管理Web + 浏览器扩展MV3
## 环境要求 ## 环境要求
- Node.js 22+ - Node.js 22+
- 包管理器npm - 包管理器npm
- Postgres仅后端直连 - Postgres仅后端直连
- OpenSpec全局安装`npm install -g @fission-ai/openspec@latest` - OpenSpec全局安装`npm install -g @fission-ai/openspec@latest`
## 开发 ## 开发
- 启动后端:`npm run dev:server` - 启动后端:`npm run dev:server`
- 启动 Web`npm run dev:web` - 启动 Web`npm run dev:web`
- 启动扩展:`npm run dev:extension` - 启动扩展:`npm run dev:extension`
- 同时启动server+web`npm run dev` - 同时启动server+web`npm run dev`
## 规范Spec-first ## 规范Spec-first
- 规范在 `spec/openapi.yaml` - 规范在 `spec/openapi.yaml`
- 校验:`npm run spec:lint` / `npm run spec:validate` - 校验:`npm run spec:lint` / `npm run spec:validate`
## 配置 ## 配置
- 复制 `.env.example``.env` 并填写数据库与密钥。 - 复制 `.env.example``.env` 并填写数据库与密钥。
## 发布NAS / SSH / x86 Linux ## 发布NAS / SSH / x86 Linux
以下步骤适用于你描述的环境x86 Linux NAS + SSH 部署,已安装 Node.js 24 与 pm2 本节记录**当前已部署的真实配置**(供 AI 直接执行/更新使用)
### 0) 本地已执行的命令(在本机开发环境 ### ✅ 当前部署清单(必须保持一致
- 路径:仓库根目录 **部署目录**
- 已执行: - 代码根目录:`/home/xxl/Code/bookmark`
- `npm install`
- `npm run build` **域名与端口**
- 前端 Web`http://mark.cloud-xl.top:9527`
这会生成下列产物: - 后端 API 代理:`http://mark.cloud-xl.top:9526`(转发到后端 3001
- Web 构建产物:`apps/web/dist/`
- 扩展构建产物:`apps/extension/dist/` **后端运行方式**
- 后端为 Node 直接运行源代码(`apps/server/src/` - systemd`browser-bookmark-server.service`
- 后端监听端口:`SERVER_PORT=3001`(由 `.env` 提供)
### 1) 服务器目录建议NAS
**前端运行方式**
建议在 NAS 上建立如下目录结构(可按需调整): - Docker 容器:`nginx`
- 端口映射:`9527 -> 80`
``` - Web 根目录(容器内):`/usr/share/nginx/html`
/opt/browser-bookmark/ - 挂载来源:`/home/xxl/Code/bookmark/apps/web/dist`
├── server/ # 后端源码与依赖
├── web/ # Web 静态站点 **API 代理方式**
└── extension/ # 扩展构建产物(用于打包/发布或本地加载) - Docker 容器:`nginx-api`
``` - 端口映射:`9526 -> 80`
- 代理目标:`http://host.docker.internal:3001`
### 2) 通过 SSH 上传文件 - 配置文件:`/home/xxl/Code/bookmark/deploy/nginx/mark.cloud-xl.top.api.conf`
以下是需要上传的内容与目标路径: **扩展包**
- 构建输出:`/home/xxl/Code/bookmark/apps/extension/dist`
1. 后端(运行源码) - 打包文件:`/home/xxl/Code/bookmark/apps/extension/extension-dist.zip`
- 本地:`apps/server/` - 下载地址:`http://mark.cloud-xl.top:9527/extension-dist.zip`
- 服务器:`/opt/browser-bookmark/server/`
**防火墙放行iptables**
2. Web 前端(静态产物) - 已放行:`9526``9527`
- 本地:`apps/web/dist/`
- 服务器:`/opt/browser-bookmark/web/` ### 1) 初次部署/重建(在服务器执行)
3. 扩展产物(打包或本地加载) **路径:`/home/xxl/Code/bookmark`**
- 本地:`apps/extension/dist/`
- 服务器:`/opt/browser-bookmark/extension/` ```bash
npm install
4. 部署配置文件 npm run build
- 本地:`deploy/` ```
- 服务器:`/opt/browser-bookmark/deploy/`
说明:
你可以使用 `scp``rsync`(示例命令需替换服务器地址与用户名): - Web 构建产物:`apps/web/dist/`
- 扩展构建产物:`apps/extension/dist/`
```bash - 后端运行仍是 `apps/server/src/`
# 后端源码
rsync -avz ./apps/server/ user@YOUR_NAS:/opt/browser-bookmark/server/ ### 2) 数据库初始化
# Web 构建产物 ```bash
rsync -avz ./apps/web/dist/ user@YOUR_NAS:/opt/browser-bookmark/web/ cd /home/xxl/Code/bookmark
npm -w apps/server run db:migrate
# 扩展构建产物 ```
rsync -avz ./apps/extension/dist/ user@YOUR_NAS:/opt/browser-bookmark/extension/
### 3) systemd 启动后端(并开机自启)
# 部署配置
rsync -avz ./deploy/ user@YOUR_NAS:/opt/browser-bookmark/deploy/ ```bash
``` sudo cp /home/xxl/Code/bookmark/deploy/systemd/browser-bookmark-server.service /etc/systemd/system/browser-bookmark-server.service
sudo systemctl daemon-reload
### 3) 服务器端安装依赖 sudo systemctl enable browser-bookmark-server
sudo systemctl start browser-bookmark-server
登录到 NAS ```
```bash ### 4) 前端 Docker Nginx9527
ssh user@YOUR_NAS
``` ```bash
sudo docker stop nginx || true
在服务器的后端目录安装依赖: sudo docker rm nginx || true
sudo docker run -d --name nginx -p 9527:80 \
```bash -v /home/xxl/Code/bookmark/deploy/nginx/mark.cloud-xl.top.conf:/etc/nginx/conf.d/default.conf:ro \
cd /opt/browser-bookmark/server -v /home/xxl/Code/bookmark/apps/web/dist:/usr/share/nginx/html:ro \
npm install --omit=dev nginx:latest
``` ```
### 4) 配置环境变量 ### 5) API 代理 Docker Nginx9526
在服务器 `server` 目录准备 `.env` ```bash
sudo docker stop nginx-api || true
```bash sudo docker rm nginx-api || true
cd /opt/browser-bookmark/server sudo docker run -d --name nginx-api -p 9526:80 \
cp .env.example .env --add-host=host.docker.internal:host-gateway \
``` -v /home/xxl/Code/bookmark/deploy/nginx/mark.cloud-xl.top.api.conf:/etc/nginx/conf.d/default.conf:ro \
nginx:latest
根据你的数据库与密钥填写 `.env` ```
### 5) 初始化数据库(首次部署必做) ### 6) 防火墙放行
```bash ```bash
cd /opt/browser-bookmark/server sudo iptables -I UG_INPUT 1 -p tcp --dport 9527 -j ACCEPT
npm run db:migrate sudo iptables -I UG_INPUT 1 -p tcp --dport 9526 -j ACCEPT
``` ```
### 6) 使用 systemd 启动后端 ### 7) 生成并发布扩展包
本仓库已生成 systemd 服务文件:`deploy/systemd/browser-bookmark-server.service` ```bash
cd /home/xxl/Code/bookmark/apps/extension/dist
将其复制到服务器并启用: zip -r ../extension-dist.zip .
cp /home/xxl/Code/bookmark/apps/extension/extension-dist.zip /home/xxl/Code/bookmark/apps/web/dist/extension-dist.zip
```bash ```
sudo cp /opt/browser-bookmark/deploy/systemd/browser-bookmark-server.service /etc/systemd/system/browser-bookmark-server.service
sudo systemctl daemon-reload ### 8) 验证清单(服务器内)
sudo systemctl enable browser-bookmark-server
sudo systemctl start browser-bookmark-server ```bash
``` curl -s http://127.0.0.1:9527 | head -n 5
curl -s http://127.0.0.1:9526/health
查看服务状态: curl -I http://127.0.0.1:9527/extension-dist.zip
```
```bash
sudo systemctl status browser-bookmark-server ### 9) 更新流程(以后改代码时)
```
> 每次修改后都按以下顺序更新,保证线上一致。
### 7) 配置 Nginx端口 6666
1) **构建**(服务器或本地)
本仓库已生成 Nginx 配置文件:`deploy/nginx/mark.cloud-xl.top.conf` - `npm install`
- `npm run build`
将其复制到服务器并启用: 2) **更新 Web 访问后端端口**
- 修改 `apps/web/.env.production` 中的 `VITE_SERVER_BASE_URL`,保持为 `http://mark.cloud-xl.top:9526`
```bash - 重新执行 `npm -w apps/web run build`
sudo cp /opt/browser-bookmark/deploy/nginx/mark.cloud-xl.top.conf /etc/nginx/conf.d/mark.cloud-xl.top.conf 3) **重启后端**
sudo nginx -t - `sudo systemctl restart browser-bookmark-server`
sudo systemctl reload nginx 4) **更新扩展包**
``` - 重新执行「生成并发布扩展包」步骤
5) **若修改 Nginx 配置**
该配置将 `mark.cloud-xl.top:6666` 指向 `/opt/browser-bookmark/web/` - 重新创建容器(见第 4/5 步)
6) **验证**
### 8) 扩展发布/加载 - 按第 8 步执行检查
扩展产物位于服务器:`/opt/browser-bookmark/extension/`
- 如需本地加载:将该目录下载到你的桌面浏览器,用“加载已解压扩展”指向该目录。
- 如需打包发布到商店:以该目录为基础打包(按平台要求)。
### 9) 验证与排查
- 后端日志:
```bash
sudo journalctl -u browser-bookmark-server -f
```
- Web 是否可访问(浏览器访问你的域名/端口)。
- 扩展是否能正常登录与同步。
### 10) 版本更新流程(建议)
1. 本地执行:`npm install` 与 `npm run build`
2. 重新上传:`apps/server/`、`apps/web/dist/`、`apps/extension/dist/`
3. 服务器端:`npm install --omit=dev`(如依赖变更)
4. 重启服务:`sudo systemctl restart browser-bookmark-server`

View File

@@ -1,5 +1,5 @@
# Extension API base (Fastify server) # Extension API base (Fastify server)
VITE_SERVER_BASE_URL=http://mark.cloud-xl.top:6667 VITE_SERVER_BASE_URL=http://mark.cloud-xl.top:6667
# Web app base (used by Options -> 跳转 Web) # Web app base (used by Options -> 跳转 Web)
VITE_WEB_BASE_URL=http://mark.cloud-xl.top:6666 VITE_WEB_BASE_URL=http://mark.cloud-xl.top:6666

View File

@@ -1,12 +1,12 @@
export default [ export default [
{ {
files: ["**/*.js", "**/*.vue"], files: ["**/*.js", "**/*.vue"],
languageOptions: { languageOptions: {
ecmaVersion: 2024, ecmaVersion: 2024,
sourceType: "module" sourceType: "module"
}, },
rules: { rules: {
"no-unused-vars": ["error", { "argsIgnorePattern": "^_" }] "no-unused-vars": ["error", { "argsIgnorePattern": "^_" }]
} }
} }
]; ];

Binary file not shown.

View File

@@ -1,12 +1,12 @@
<!doctype html> <!doctype html>
<html lang="zh-CN"> <html lang="zh-CN">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>云书签 选项</title> <title>云书签 选项</title>
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>
<script type="module" src="/src/options/main.js"></script> <script type="module" src="/src/options/main.js"></script>
</body> </body>
</html> </html>

View File

@@ -11,7 +11,7 @@
"lint": "eslint ." "lint": "eslint ."
}, },
"dependencies": { "dependencies": {
"@browser-bookmark/shared": "0.1.0", "@browser-bookmark/shared": "file:../../packages/shared",
"vue-router": "^4.5.1", "vue-router": "^4.5.1",
"vue": "^3.5.24" "vue": "^3.5.24"
}, },

View File

@@ -1,12 +1,12 @@
<!doctype html> <!doctype html>
<html lang="zh-CN"> <html lang="zh-CN">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Bookmarks</title> <title>Bookmarks</title>
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>
<script type="module" src="/src/popup/main.js"></script> <script type="module" src="/src/popup/main.js"></script>
</body> </body>
</html> </html>

View File

@@ -1,12 +1,12 @@
{ {
"manifest_version": 3, "manifest_version": 3,
"name": "BrowserBookmark", "name": "BrowserBookmark",
"version": "0.1.0", "version": "0.1.0",
"action": { "action": {
"default_title": "Bookmarks", "default_title": "Bookmarks",
"default_popup": "popup.html" "default_popup": "popup.html"
}, },
"options_page": "options.html", "options_page": "options.html",
"permissions": ["storage", "tabs"], "permissions": ["storage", "tabs"],
"host_permissions": ["<all_urls>"] "host_permissions": ["<all_urls>"]
} }

View File

@@ -1,154 +1,154 @@
<script setup> <script setup>
import { onBeforeUnmount, onMounted } from "vue"; import { onBeforeUnmount, onMounted } from "vue";
const props = defineProps({ const props = defineProps({
modelValue: { type: Boolean, default: false }, modelValue: { type: Boolean, default: false },
title: { type: String, default: "请确认" }, title: { type: String, default: "请确认" },
message: { type: String, default: "" }, message: { type: String, default: "" },
confirmText: { type: String, default: "确定" }, confirmText: { type: String, default: "确定" },
cancelText: { type: String, default: "取消" }, cancelText: { type: String, default: "取消" },
danger: { type: Boolean, default: false }, danger: { type: Boolean, default: false },
maxWidth: { type: String, default: "520px" } maxWidth: { type: String, default: "520px" }
}); });
const emit = defineEmits(["update:modelValue", "confirm", "cancel"]); const emit = defineEmits(["update:modelValue", "confirm", "cancel"]);
function cancel() { function cancel() {
emit("update:modelValue", false); emit("update:modelValue", false);
emit("cancel"); emit("cancel");
} }
function confirm() { function confirm() {
emit("confirm"); emit("confirm");
} }
function onKeydown(e) { function onKeydown(e) {
if (e.key === "Escape") cancel(); if (e.key === "Escape") cancel();
} }
onMounted(() => { onMounted(() => {
window.addEventListener("keydown", onKeydown); window.addEventListener("keydown", onKeydown);
}); });
onBeforeUnmount(() => { onBeforeUnmount(() => {
window.removeEventListener("keydown", onKeydown); window.removeEventListener("keydown", onKeydown);
}); });
</script> </script>
<template> <template>
<teleport to="body"> <teleport to="body">
<div v-if="modelValue" class="bb-modalOverlay" role="dialog" aria-modal="true"> <div v-if="modelValue" class="bb-modalOverlay" role="dialog" aria-modal="true">
<div class="bb-modalBackdrop" @click="cancel" /> <div class="bb-modalBackdrop" @click="cancel" />
<div class="bb-modalPanel" :style="{ maxWidth }"> <div class="bb-modalPanel" :style="{ maxWidth }">
<div class="bb-modalHeader"> <div class="bb-modalHeader">
<div class="bb-modalTitle">{{ title }}</div> <div class="bb-modalTitle">{{ title }}</div>
<button type="button" class="bb-modalClose" @click="cancel" aria-label="关闭">×</button> <button type="button" class="bb-modalClose" @click="cancel" aria-label="关闭">×</button>
</div> </div>
<div class="bb-modalBody"> <div class="bb-modalBody">
<div v-if="message" class="bb-modalMessage">{{ message }}</div> <div v-if="message" class="bb-modalMessage">{{ message }}</div>
<slot /> <slot />
<div class="bb-modalActions"> <div class="bb-modalActions">
<button type="button" class="bb-btn bb-btn--secondary" @click="cancel">{{ cancelText }}</button> <button type="button" class="bb-btn bb-btn--secondary" @click="cancel">{{ cancelText }}</button>
<button type="button" class="bb-btn" :class="danger ? 'bb-btn--danger' : ''" @click="confirm">{{ confirmText }}</button> <button type="button" class="bb-btn" :class="danger ? 'bb-btn--danger' : ''" @click="confirm">{{ confirmText }}</button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</teleport> </teleport>
</template> </template>
<style scoped> <style scoped>
.bb-modalOverlay { .bb-modalOverlay {
position: fixed; position: fixed;
inset: 0; inset: 0;
z-index: 2147483500; z-index: 2147483500;
display: grid; display: grid;
place-items: center; place-items: center;
padding: 16px; padding: 16px;
} }
.bb-modalBackdrop { .bb-modalBackdrop {
position: absolute; position: absolute;
inset: 0; inset: 0;
background: rgba(15, 23, 42, 0.38); background: rgba(15, 23, 42, 0.38);
backdrop-filter: blur(8px); backdrop-filter: blur(8px);
} }
.bb-modalPanel { .bb-modalPanel {
position: relative; position: relative;
width: min(100%, 560px); width: min(100%, 560px);
max-height: min(84vh, 860px); max-height: min(84vh, 860px);
overflow: auto; overflow: auto;
border-radius: 18px; border-radius: 18px;
border: 1px solid rgba(255, 255, 255, 0.7); border: 1px solid rgba(255, 255, 255, 0.7);
background: rgba(255, 255, 255, 0.82); background: rgba(255, 255, 255, 0.82);
box-shadow: 0 18px 60px rgba(15, 23, 42, 0.22); box-shadow: 0 18px 60px rgba(15, 23, 42, 0.22);
color: var(--bb-text); color: var(--bb-text);
} }
.bb-modalHeader { .bb-modalHeader {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
gap: 10px; gap: 10px;
padding: 12px 14px; padding: 12px 14px;
border-bottom: 1px solid rgba(15, 23, 42, 0.1); border-bottom: 1px solid rgba(15, 23, 42, 0.1);
} }
.bb-modalTitle { .bb-modalTitle {
font-weight: 900; font-weight: 900;
} }
.bb-modalClose { .bb-modalClose {
width: 34px; width: 34px;
height: 34px; height: 34px;
border-radius: 12px; border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.6); border: 1px solid rgba(255, 255, 255, 0.6);
background: rgba(255, 255, 255, 0.5); background: rgba(255, 255, 255, 0.5);
cursor: pointer; cursor: pointer;
font-size: 20px; font-size: 20px;
line-height: 1; line-height: 1;
color: rgba(15, 23, 42, 0.75); color: rgba(15, 23, 42, 0.75);
} }
.bb-modalClose:hover { .bb-modalClose:hover {
background: rgba(255, 255, 255, 0.78); background: rgba(255, 255, 255, 0.78);
} }
.bb-modalBody { .bb-modalBody {
padding: 14px; padding: 14px;
} }
.bb-modalMessage { .bb-modalMessage {
color: rgba(15, 23, 42, 0.78); color: rgba(15, 23, 42, 0.78);
font-size: 13px; font-size: 13px;
line-height: 1.6; line-height: 1.6;
white-space: pre-wrap; white-space: pre-wrap;
} }
.bb-modalActions { .bb-modalActions {
margin-top: 14px; margin-top: 14px;
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
gap: 10px; gap: 10px;
} }
.bb-btn { .bb-btn {
border: 1px solid rgba(255, 255, 255, 0.25); border: 1px solid rgba(255, 255, 255, 0.25);
border-radius: 14px; border-radius: 14px;
padding: 10px 12px; padding: 10px 12px;
background: linear-gradient(135deg, var(--bb-primary), var(--bb-cta)); background: linear-gradient(135deg, var(--bb-primary), var(--bb-cta));
color: white; color: white;
cursor: pointer; cursor: pointer;
} }
.bb-btn--secondary { .bb-btn--secondary {
background: rgba(255, 255, 255, 0.55); background: rgba(255, 255, 255, 0.55);
color: var(--bb-text); color: var(--bb-text);
border-color: var(--bb-border); border-color: var(--bb-border);
} }
.bb-btn--danger { .bb-btn--danger {
background: linear-gradient(135deg, #ef4444, #f97316); background: linear-gradient(135deg, #ef4444, #f97316);
} }
</style> </style>

View File

@@ -1,31 +1,31 @@
const BASE_URL = import.meta.env.VITE_SERVER_BASE_URL || "http://localhost:3001"; const BASE_URL = import.meta.env.VITE_SERVER_BASE_URL || "http://localhost:3001";
export async function apiFetch(path, options = {}) { export async function apiFetch(path, options = {}) {
const headers = new Headers(options.headers || {}); const headers = new Headers(options.headers || {});
if (!headers.has("Accept")) headers.set("Accept", "application/json"); if (!headers.has("Accept")) headers.set("Accept", "application/json");
if (!(options.body instanceof FormData) && options.body != null) { if (!(options.body instanceof FormData) && options.body != null) {
headers.set("Content-Type", "application/json"); headers.set("Content-Type", "application/json");
} }
// Lazy import to avoid circular deps // Lazy import to avoid circular deps
const { getToken } = await import("./extStorage.js"); const { getToken } = await import("./extStorage.js");
const token = await getToken(); const token = await getToken();
if (token) headers.set("Authorization", `Bearer ${token}`); if (token) headers.set("Authorization", `Bearer ${token}`);
const res = await fetch(`${BASE_URL}${path}`, { ...options, headers }); const res = await fetch(`${BASE_URL}${path}`, { ...options, headers });
const contentType = res.headers.get("content-type") || ""; const contentType = res.headers.get("content-type") || "";
const isJson = contentType.includes("application/json"); const isJson = contentType.includes("application/json");
const payload = isJson ? await res.json().catch(() => null) : await res.text().catch(() => ""); const payload = isJson ? await res.json().catch(() => null) : await res.text().catch(() => "");
if (!res.ok) { if (!res.ok) {
const message = payload?.message || `HTTP ${res.status}`; const message = payload?.message || `HTTP ${res.status}`;
const err = new Error(message); const err = new Error(message);
err.status = res.status; err.status = res.status;
err.payload = payload; err.payload = payload;
throw err; throw err;
} }
return payload; return payload;
} }

View File

@@ -1,43 +1,43 @@
const TOKEN_KEY = "bb_token"; const TOKEN_KEY = "bb_token";
const LOCAL_STATE_KEY = "bb_local_state_v1"; const LOCAL_STATE_KEY = "bb_local_state_v1";
function hasChromeStorage() { function hasChromeStorage() {
return typeof chrome !== "undefined" && chrome.storage?.local; return typeof chrome !== "undefined" && chrome.storage?.local;
} }
export async function getToken() { export async function getToken() {
if (hasChromeStorage()) { if (hasChromeStorage()) {
const res = await chrome.storage.local.get([TOKEN_KEY]); const res = await chrome.storage.local.get([TOKEN_KEY]);
return res[TOKEN_KEY] || ""; return res[TOKEN_KEY] || "";
} }
return localStorage.getItem(TOKEN_KEY) || ""; return localStorage.getItem(TOKEN_KEY) || "";
} }
export async function setToken(token) { export async function setToken(token) {
if (hasChromeStorage()) { if (hasChromeStorage()) {
await chrome.storage.local.set({ [TOKEN_KEY]: token || "" }); await chrome.storage.local.set({ [TOKEN_KEY]: token || "" });
return; return;
} }
if (token) localStorage.setItem(TOKEN_KEY, token); if (token) localStorage.setItem(TOKEN_KEY, token);
else localStorage.removeItem(TOKEN_KEY); else localStorage.removeItem(TOKEN_KEY);
} }
export async function loadLocalState() { export async function loadLocalState() {
if (hasChromeStorage()) { if (hasChromeStorage()) {
const res = await chrome.storage.local.get([LOCAL_STATE_KEY]); const res = await chrome.storage.local.get([LOCAL_STATE_KEY]);
return res[LOCAL_STATE_KEY] || { folders: [], bookmarks: [] }; return res[LOCAL_STATE_KEY] || { folders: [], bookmarks: [] };
} }
try { try {
return JSON.parse(localStorage.getItem(LOCAL_STATE_KEY) || "") || { folders: [], bookmarks: [] }; return JSON.parse(localStorage.getItem(LOCAL_STATE_KEY) || "") || { folders: [], bookmarks: [] };
} catch { } catch {
return { folders: [], bookmarks: [] }; return { folders: [], bookmarks: [] };
} }
} }
export async function saveLocalState(state) { export async function saveLocalState(state) {
if (hasChromeStorage()) { if (hasChromeStorage()) {
await chrome.storage.local.set({ [LOCAL_STATE_KEY]: state }); await chrome.storage.local.set({ [LOCAL_STATE_KEY]: state });
return; return;
} }
localStorage.setItem(LOCAL_STATE_KEY, JSON.stringify(state)); localStorage.setItem(LOCAL_STATE_KEY, JSON.stringify(state));
} }

View File

@@ -1,218 +1,218 @@
import { computeUrlHash, normalizeUrl, parseNetscapeBookmarkHtml } from "@browser-bookmark/shared"; import { computeUrlHash, normalizeUrl, parseNetscapeBookmarkHtml } from "@browser-bookmark/shared";
import { loadLocalState, saveLocalState } from "./extStorage"; import { loadLocalState, saveLocalState } from "./extStorage";
function nowIso() { function nowIso() {
return new Date().toISOString(); return new Date().toISOString();
} }
function ensureBookmarkHashes(bookmark) { function ensureBookmarkHashes(bookmark) {
const urlNormalized = bookmark.urlNormalized || normalizeUrl(bookmark.url || ""); const urlNormalized = bookmark.urlNormalized || normalizeUrl(bookmark.url || "");
const urlHash = bookmark.urlHash || computeUrlHash(urlNormalized); const urlHash = bookmark.urlHash || computeUrlHash(urlNormalized);
return { ...bookmark, urlNormalized, urlHash }; return { ...bookmark, urlNormalized, urlHash };
} }
export async function listLocalBookmarks({ includeDeleted = false } = {}) { export async function listLocalBookmarks({ includeDeleted = false } = {}) {
const state = await loadLocalState(); const state = await loadLocalState();
const items = (state.bookmarks || []).map(ensureBookmarkHashes); const items = (state.bookmarks || []).map(ensureBookmarkHashes);
const filtered = includeDeleted ? items : items.filter((b) => !b.deletedAt); const filtered = includeDeleted ? items : items.filter((b) => !b.deletedAt);
// Keep newest first // Keep newest first
filtered.sort((a, b) => String(b.updatedAt || "").localeCompare(String(a.updatedAt || ""))); filtered.sort((a, b) => String(b.updatedAt || "").localeCompare(String(a.updatedAt || "")));
return filtered; return filtered;
} }
export async function upsertLocalBookmark({ title, url, visibility = "public", folderId = null, source = "manual" }) { export async function upsertLocalBookmark({ title, url, visibility = "public", folderId = null, source = "manual" }) {
const state = await loadLocalState(); const state = await loadLocalState();
const now = nowIso(); const now = nowIso();
const urlNormalized = normalizeUrl(url || ""); const urlNormalized = normalizeUrl(url || "");
const urlHash = computeUrlHash(urlNormalized); const urlHash = computeUrlHash(urlNormalized);
// Dedupe: same urlHash and not deleted -> update LWW // Dedupe: same urlHash and not deleted -> update LWW
const existing = (state.bookmarks || []).find((b) => !b.deletedAt && (b.urlHash || "") === urlHash); const existing = (state.bookmarks || []).find((b) => !b.deletedAt && (b.urlHash || "") === urlHash);
if (existing) { if (existing) {
existing.title = title || existing.title; existing.title = title || existing.title;
existing.url = url || existing.url; existing.url = url || existing.url;
existing.urlNormalized = urlNormalized; existing.urlNormalized = urlNormalized;
existing.urlHash = urlHash; existing.urlHash = urlHash;
existing.visibility = visibility; existing.visibility = visibility;
existing.folderId = folderId; existing.folderId = folderId;
existing.source = source; existing.source = source;
existing.updatedAt = now; existing.updatedAt = now;
await saveLocalState(state); await saveLocalState(state);
return { bookmark: ensureBookmarkHashes(existing), merged: true }; return { bookmark: ensureBookmarkHashes(existing), merged: true };
} }
const bookmark = { const bookmark = {
id: crypto.randomUUID(), id: crypto.randomUUID(),
userId: null, userId: null,
folderId, folderId,
title: title || "", title: title || "",
url: url || "", url: url || "",
urlNormalized, urlNormalized,
urlHash, urlHash,
visibility, visibility,
source, source,
updatedAt: now, updatedAt: now,
deletedAt: null deletedAt: null
}; };
state.bookmarks = state.bookmarks || []; state.bookmarks = state.bookmarks || [];
state.bookmarks.unshift(bookmark); state.bookmarks.unshift(bookmark);
await saveLocalState(state); await saveLocalState(state);
return { bookmark: ensureBookmarkHashes(bookmark), merged: false }; return { bookmark: ensureBookmarkHashes(bookmark), merged: false };
} }
export async function markLocalDeleted(id) { export async function markLocalDeleted(id) {
const state = await loadLocalState(); const state = await loadLocalState();
const now = nowIso(); const now = nowIso();
const item = (state.bookmarks || []).find((b) => b.id === id); const item = (state.bookmarks || []).find((b) => b.id === id);
if (!item) return false; if (!item) return false;
item.deletedAt = now; item.deletedAt = now;
item.updatedAt = now; item.updatedAt = now;
await saveLocalState(state); await saveLocalState(state);
return true; return true;
} }
export async function clearLocalState() { export async function clearLocalState() {
await saveLocalState({ folders: [], bookmarks: [] }); await saveLocalState({ folders: [], bookmarks: [] });
} }
export async function mergeLocalToUser() { export async function mergeLocalToUser() {
const state = await loadLocalState(); const state = await loadLocalState();
return { return {
folders: (state.folders || []).map((f) => ({ folders: (state.folders || []).map((f) => ({
id: f.id, id: f.id,
parentId: f.parentId ?? null, parentId: f.parentId ?? null,
name: f.name || "", name: f.name || "",
visibility: f.visibility || "private", visibility: f.visibility || "private",
updatedAt: f.updatedAt || nowIso() updatedAt: f.updatedAt || nowIso()
})), })),
bookmarks: (state.bookmarks || []).map((b) => { bookmarks: (state.bookmarks || []).map((b) => {
const fixed = ensureBookmarkHashes(b); const fixed = ensureBookmarkHashes(b);
return { return {
id: fixed.id, id: fixed.id,
folderId: fixed.folderId ?? null, folderId: fixed.folderId ?? null,
title: fixed.title || "", title: fixed.title || "",
url: fixed.url || "", url: fixed.url || "",
visibility: fixed.visibility || "private", visibility: fixed.visibility || "private",
source: fixed.source || "manual", source: fixed.source || "manual",
updatedAt: fixed.updatedAt || nowIso(), updatedAt: fixed.updatedAt || nowIso(),
deletedAt: fixed.deletedAt || null deletedAt: fixed.deletedAt || null
}; };
}) })
}; };
} }
export async function importLocalFromNetscapeHtml(html, { visibility = "public" } = {}) { export async function importLocalFromNetscapeHtml(html, { visibility = "public" } = {}) {
const parsed = parseNetscapeBookmarkHtml(html || ""); const parsed = parseNetscapeBookmarkHtml(html || "");
const state = await loadLocalState(); const state = await loadLocalState();
const now = nowIso(); const now = nowIso();
state.folders = state.folders || []; state.folders = state.folders || [];
state.bookmarks = state.bookmarks || []; state.bookmarks = state.bookmarks || [];
// Folder id remap (avoid collisions with existing UUID ids) // Folder id remap (avoid collisions with existing UUID ids)
const folderIdMap = new Map(); const folderIdMap = new Map();
for (const f of parsed.folders || []) { for (const f of parsed.folders || []) {
folderIdMap.set(f.id, crypto.randomUUID()); folderIdMap.set(f.id, crypto.randomUUID());
} }
// Dedupe folders by (parentId,name) // Dedupe folders by (parentId,name)
const folderKeyToId = new Map( const folderKeyToId = new Map(
state.folders.map((f) => [`${f.parentId ?? ""}::${(f.name || "").toLowerCase()}`, f.id]) state.folders.map((f) => [`${f.parentId ?? ""}::${(f.name || "").toLowerCase()}`, f.id])
); );
const oldFolderIdToActual = new Map(); const oldFolderIdToActual = new Map();
let foldersImported = 0; let foldersImported = 0;
for (const f of parsed.folders || []) { for (const f of parsed.folders || []) {
const parentId = f.parentFolderId ? oldFolderIdToActual.get(f.parentFolderId) || null : null; const parentId = f.parentFolderId ? oldFolderIdToActual.get(f.parentFolderId) || null : null;
const name = (f.name || "").trim(); const name = (f.name || "").trim();
const key = `${parentId ?? ""}::${name.toLowerCase()}`; const key = `${parentId ?? ""}::${name.toLowerCase()}`;
let id = folderKeyToId.get(key); let id = folderKeyToId.get(key);
if (!id) { if (!id) {
id = folderIdMap.get(f.id); id = folderIdMap.get(f.id);
state.folders.push({ state.folders.push({
id, id,
userId: null, userId: null,
parentId, parentId,
name, name,
visibility, visibility,
updatedAt: now updatedAt: now
}); });
folderKeyToId.set(key, id); folderKeyToId.set(key, id);
foldersImported++; foldersImported++;
} else { } else {
// Keep existing, but ensure it has a recent updatedAt // Keep existing, but ensure it has a recent updatedAt
const existing = state.folders.find((x) => x.id === id); const existing = state.folders.find((x) => x.id === id);
if (existing && (!existing.updatedAt || existing.updatedAt < now)) existing.updatedAt = now; if (existing && (!existing.updatedAt || existing.updatedAt < now)) existing.updatedAt = now;
} }
oldFolderIdToActual.set(f.id, id); oldFolderIdToActual.set(f.id, id);
} }
// Dedupe bookmarks by urlHash // Dedupe bookmarks by urlHash
const existingByHash = new Map(); const existingByHash = new Map();
for (const b of state.bookmarks.map(ensureBookmarkHashes)) { for (const b of state.bookmarks.map(ensureBookmarkHashes)) {
if (!b.deletedAt && b.urlHash) existingByHash.set(b.urlHash, b); if (!b.deletedAt && b.urlHash) existingByHash.set(b.urlHash, b);
} }
let imported = 0; let imported = 0;
let merged = 0; let merged = 0;
for (const b of parsed.bookmarks || []) { for (const b of parsed.bookmarks || []) {
const title = (b.title || "").trim(); const title = (b.title || "").trim();
const url = (b.url || "").trim(); const url = (b.url || "").trim();
if (!url) continue; if (!url) continue;
const urlNormalized = normalizeUrl(url); const urlNormalized = normalizeUrl(url);
const urlHash = computeUrlHash(urlNormalized); const urlHash = computeUrlHash(urlNormalized);
const folderId = b.parentFolderId ? oldFolderIdToActual.get(b.parentFolderId) || null : null; const folderId = b.parentFolderId ? oldFolderIdToActual.get(b.parentFolderId) || null : null;
const existing = existingByHash.get(urlHash); const existing = existingByHash.get(urlHash);
if (existing) { if (existing) {
existing.title = title || existing.title; existing.title = title || existing.title;
existing.updatedAt = now; existing.updatedAt = now;
merged++; merged++;
continue; continue;
} }
state.bookmarks.unshift({ state.bookmarks.unshift({
id: crypto.randomUUID(), id: crypto.randomUUID(),
userId: null, userId: null,
folderId: folderId ?? null, folderId: folderId ?? null,
title, title,
url, url,
urlNormalized, urlNormalized,
urlHash, urlHash,
visibility, visibility,
source: "import", source: "import",
updatedAt: now, updatedAt: now,
deletedAt: null deletedAt: null
}); });
existingByHash.set(urlHash, state.bookmarks[0]); existingByHash.set(urlHash, state.bookmarks[0]);
imported++; imported++;
} }
await saveLocalState(state); await saveLocalState(state);
return { foldersImported, imported, merged }; return { foldersImported, imported, merged };
} }
export function exportLocalToNetscapeHtml(bookmarks) { export function exportLocalToNetscapeHtml(bookmarks) {
const safe = (s) => String(s || "").replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;"); const safe = (s) => String(s || "").replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
const lines = []; const lines = [];
lines.push("<!DOCTYPE NETSCAPE-Bookmark-file-1>"); lines.push("<!DOCTYPE NETSCAPE-Bookmark-file-1>");
lines.push('<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8">'); lines.push('<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8">');
lines.push("<TITLE>Bookmarks</TITLE>"); lines.push("<TITLE>Bookmarks</TITLE>");
lines.push("<H1>Bookmarks</H1>"); lines.push("<H1>Bookmarks</H1>");
lines.push("<DL><p>"); lines.push("<DL><p>");
for (const b of bookmarks || []) { for (const b of bookmarks || []) {
if (b.deletedAt) continue; if (b.deletedAt) continue;
lines.push(` <DT><A HREF="${safe(b.url)}">${safe(b.title || b.url)}</A>`); lines.push(` <DT><A HREF="${safe(b.url)}">${safe(b.title || b.url)}</A>`);
} }
lines.push("</DL><p>"); lines.push("</DL><p>");
return lines.join("\n"); return lines.join("\n");
} }

View File

@@ -1,45 +1,45 @@
<script setup> <script setup>
import { RouterView } from "vue-router"; import { RouterView } from "vue-router";
</script> </script>
<template> <template>
<div class="shell"> <div class="shell">
<header class="nav"> <header class="nav">
<div class="brand">云书签 · 更多操作</div> <div class="brand">云书签 · 更多操作</div>
</header> </header>
<main class="content"> <main class="content">
<RouterView /> <RouterView />
</main> </main>
</div> </div>
</template> </template>
<style> <style>
body { margin: 0; } body { margin: 0; }
.shell { min-height: 100vh; } .shell { min-height: 100vh; }
.nav { .nav {
position: sticky; position: sticky;
top: 0; top: 0;
background: rgba(248, 250, 252, 0.82); background: rgba(248, 250, 252, 0.82);
border-bottom: 1px solid var(--bb-border); border-bottom: 1px solid var(--bb-border);
backdrop-filter: blur(10px); backdrop-filter: blur(10px);
display: flex; display: flex;
justify-content: flex-start; justify-content: flex-start;
padding: 10px 14px; padding: 10px 14px;
gap: 10px; gap: 10px;
} }
.brand { font-weight: 800; } .brand { font-weight: 800; }
.content { max-width: 1100px; margin: 0 auto; padding: 14px; } .content { max-width: 1100px; margin: 0 auto; padding: 14px; }
a:focus-visible, a:focus-visible,
button:focus-visible, button:focus-visible,
input:focus-visible { input:focus-visible {
outline: 2px solid rgba(59, 130, 246, 0.6); outline: 2px solid rgba(59, 130, 246, 0.6);
outline-offset: 2px; outline-offset: 2px;
} }
@media (prefers-reduced-motion: reduce) { @media (prefers-reduced-motion: reduce) {
* { transition: none !important; scroll-behavior: auto !important; } * { transition: none !important; scroll-behavior: auto !important; }
} }
</style> </style>

View File

@@ -1,7 +1,7 @@
import { createApp } from "vue"; import { createApp } from "vue";
import OptionsApp from "./OptionsApp.vue"; import OptionsApp from "./OptionsApp.vue";
import { router } from "./router"; import { router } from "./router";
import "../style.css"; import "../style.css";
createApp(OptionsApp).use(router).mount("#app"); createApp(OptionsApp).use(router).mount("#app");

View File

@@ -1,135 +1,135 @@
<script setup> <script setup>
import { ref } from "vue"; import { ref } from "vue";
import { apiFetch } from "../../lib/api"; import { apiFetch } from "../../lib/api";
import { getToken } from "../../lib/extStorage"; import { getToken } from "../../lib/extStorage";
import { exportLocalToNetscapeHtml, importLocalFromNetscapeHtml, listLocalBookmarks } from "../../lib/localData"; import { exportLocalToNetscapeHtml, importLocalFromNetscapeHtml, listLocalBookmarks } from "../../lib/localData";
const file = ref(null); const file = ref(null);
const status = ref(""); const status = ref("");
const error = ref(""); const error = ref("");
function onFileChange(e) { function onFileChange(e) {
file.value = e?.target?.files?.[0] || null; file.value = e?.target?.files?.[0] || null;
} }
async function importToLocal() { async function importToLocal() {
status.value = ""; status.value = "";
error.value = ""; error.value = "";
if (!file.value) return; if (!file.value) return;
try { try {
const text = await file.value.text(); const text = await file.value.text();
const res = await importLocalFromNetscapeHtml(text, { visibility: "public" }); const res = await importLocalFromNetscapeHtml(text, { visibility: "public" });
status.value = `本地导入完成:文件夹新增 ${res.foldersImported},书签新增 ${res.imported},合并 ${res.merged}`; status.value = `本地导入完成:文件夹新增 ${res.foldersImported},书签新增 ${res.imported},合并 ${res.merged}`;
} catch (e) { } catch (e) {
error.value = e.message || String(e); error.value = e.message || String(e);
} }
} }
async function importFile() { async function importFile() {
status.value = ""; status.value = "";
error.value = ""; error.value = "";
const token = await getToken(); const token = await getToken();
if (!token) { if (!token) {
await importToLocal(); await importToLocal();
return; return;
} }
if (!file.value) return; if (!file.value) return;
try { try {
const fd = new FormData(); const fd = new FormData();
fd.append("file", file.value); fd.append("file", file.value);
const res = await apiFetch("/bookmarks/import/html", { method: "POST", body: fd }); const res = await apiFetch("/bookmarks/import/html", { method: "POST", body: fd });
status.value = `导入完成:新增 ${res.imported},合并 ${res.merged}`; status.value = `导入完成:新增 ${res.imported},合并 ${res.merged}`;
} catch (e) { } catch (e) {
error.value = e.message || String(e); error.value = e.message || String(e);
} }
} }
async function exportCloud() { async function exportCloud() {
status.value = ""; status.value = "";
error.value = ""; error.value = "";
try { try {
const html = await apiFetch("/bookmarks/export/html", { const html = await apiFetch("/bookmarks/export/html", {
method: "GET", method: "GET",
headers: { Accept: "text/html" } headers: { Accept: "text/html" }
}); });
const blob = new Blob([html], { type: "text/html;charset=utf-8" }); const blob = new Blob([html], { type: "text/html;charset=utf-8" });
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
const a = document.createElement("a"); const a = document.createElement("a");
a.href = url; a.href = url;
a.download = "bookmarks-cloud.html"; a.download = "bookmarks-cloud.html";
document.body.appendChild(a); document.body.appendChild(a);
a.click(); a.click();
a.remove(); a.remove();
URL.revokeObjectURL(url); URL.revokeObjectURL(url);
status.value = "云端导出完成"; status.value = "云端导出完成";
} catch (e) { } catch (e) {
error.value = e.message || String(e); error.value = e.message || String(e);
} }
} }
async function exportLocal() { async function exportLocal() {
status.value = ""; status.value = "";
error.value = ""; error.value = "";
try { try {
const bookmarks = await listLocalBookmarks({ includeDeleted: false }); const bookmarks = await listLocalBookmarks({ includeDeleted: false });
const html = exportLocalToNetscapeHtml(bookmarks); const html = exportLocalToNetscapeHtml(bookmarks);
const blob = new Blob([html], { type: "text/html;charset=utf-8" }); const blob = new Blob([html], { type: "text/html;charset=utf-8" });
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
const a = document.createElement("a"); const a = document.createElement("a");
a.href = url; a.href = url;
a.download = "bookmarks.html"; a.download = "bookmarks.html";
document.body.appendChild(a); document.body.appendChild(a);
a.click(); a.click();
a.remove(); a.remove();
URL.revokeObjectURL(url); URL.revokeObjectURL(url);
status.value = `本地导出完成:${bookmarks.length}`; status.value = `本地导出完成:${bookmarks.length}`;
} catch (e) { } catch (e) {
error.value = e.message || String(e); error.value = e.message || String(e);
} }
} }
</script> </script>
<template> <template>
<section> <section>
<h1>导入 / 导出</h1> <h1>导入 / 导出</h1>
<div class="card"> <div class="card">
<h2>导入书签 HTML写入云端</h2> <h2>导入书签 HTML写入云端</h2>
<input <input
type="file" type="file"
accept="text/html,.html" accept="text/html,.html"
@change="onFileChange" @change="onFileChange"
/> />
<button class="btn" @click="importFile">开始导入</button> <button class="btn" @click="importFile">开始导入</button>
<p v-if="status" class="ok">{{ status }}</p> <p v-if="status" class="ok">{{ status }}</p>
<p v-if="error" class="error">{{ error }}</p> <p v-if="error" class="error">{{ error }}</p>
</div> </div>
<div class="card"> <div class="card">
<h2>导出本地</h2> <h2>导出本地</h2>
<button class="btn" @click="exportLocal">导出本地为 HTML</button> <button class="btn" @click="exportLocal">导出本地为 HTML</button>
</div> </div>
<div class="card"> <div class="card">
<h2>导出云端</h2> <h2>导出云端</h2>
<button class="btn" @click="exportCloud">导出为 HTML</button> <button class="btn" @click="exportCloud">导出为 HTML</button>
</div> </div>
</section> </section>
</template> </template>
<style scoped> <style scoped>
.card { border: 1px solid #e5e7eb; border-radius: 14px; padding: 12px; background: white; margin: 12px 0; } .card { border: 1px solid #e5e7eb; border-radius: 14px; padding: 12px; background: white; margin: 12px 0; }
.btn { margin-top: 10px; padding: 10px 12px; border: 1px solid #111827; border-radius: 10px; background: #111827; color: white; cursor: pointer; } .btn { margin-top: 10px; padding: 10px 12px; border: 1px solid #111827; border-radius: 10px; background: #111827; color: white; cursor: pointer; }
.ok { color: #065f46; } .ok { color: #065f46; }
.error { color: #b91c1c; } .error { color: #b91c1c; }
</style> </style>

View File

@@ -1,77 +1,77 @@
<script setup> <script setup>
import { ref } from "vue"; import { ref } from "vue";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import { apiFetch } from "../../lib/api"; import { apiFetch } from "../../lib/api";
import { setToken } from "../../lib/extStorage"; import { setToken } from "../../lib/extStorage";
import { clearLocalState, mergeLocalToUser } from "../../lib/localData"; import { clearLocalState, mergeLocalToUser } from "../../lib/localData";
const router = useRouter(); const router = useRouter();
const mode = ref("login"); const mode = ref("login");
const email = ref(""); const email = ref("");
const password = ref(""); const password = ref("");
const error = ref(""); const error = ref("");
const loading = ref(false); const loading = ref(false);
async function submit() { async function submit() {
loading.value = true; loading.value = true;
error.value = ""; error.value = "";
try { try {
const path = mode.value === "register" ? "/auth/register" : "/auth/login"; const path = mode.value === "register" ? "/auth/register" : "/auth/login";
const res = await apiFetch(path, { const res = await apiFetch(path, {
method: "POST", method: "POST",
body: JSON.stringify({ email: email.value, password: password.value }) body: JSON.stringify({ email: email.value, password: password.value })
}); });
await setToken(res.token); await setToken(res.token);
// Push local state to server on login // Push local state to server on login
const payload = await mergeLocalToUser(); const payload = await mergeLocalToUser();
await apiFetch("/sync/push", { await apiFetch("/sync/push", {
method: "POST", method: "POST",
body: JSON.stringify(payload) body: JSON.stringify(payload)
}); });
// After merge, keep extension in cloud mode // After merge, keep extension in cloud mode
await clearLocalState(); await clearLocalState();
await router.replace("/"); await router.replace("/");
} catch (e) { } catch (e) {
error.value = e.message || String(e); error.value = e.message || String(e);
} finally { } finally {
loading.value = false; loading.value = false;
} }
} }
</script> </script>
<template> <template>
<section> <section>
<h1>登录 / 注册</h1> <h1>登录 / 注册</h1>
<div class="row"> <div class="row">
<button class="tab" :class="{ active: mode === 'login' }" @click="mode = 'login'">登录</button> <button class="tab" :class="{ active: mode === 'login' }" @click="mode = 'login'">登录</button>
<button class="tab" :class="{ active: mode === 'register' }" @click="mode = 'register'">注册</button> <button class="tab" :class="{ active: mode === 'register' }" @click="mode = 'register'">注册</button>
</div> </div>
<div class="form"> <div class="form">
<input v-model="email" class="input" placeholder="邮箱" autocomplete="email" /> <input v-model="email" class="input" placeholder="邮箱" autocomplete="email" />
<input v-model="password" class="input" placeholder="密码(至少 8 位)" type="password" autocomplete="current-password" /> <input v-model="password" class="input" placeholder="密码(至少 8 位)" type="password" autocomplete="current-password" />
<button class="btn" :disabled="loading" @click="submit">提交</button> <button class="btn" :disabled="loading" @click="submit">提交</button>
<p v-if="error" class="error">{{ error }}</p> <p v-if="error" class="error">{{ error }}</p>
</div> </div>
<p class="muted">扩展内的本地书签存储在 chrome.storage.local</p> <p class="muted">扩展内的本地书签存储在 chrome.storage.local</p>
</section> </section>
</template> </template>
<style scoped> <style scoped>
.row { display: flex; gap: 8px; margin: 12px 0; flex-wrap: wrap; } .row { display: flex; gap: 8px; margin: 12px 0; flex-wrap: wrap; }
.tab { padding: 10px 12px; border: 1px solid #e5e7eb; border-radius: 10px; background: #f8fafc; cursor: pointer; } .tab { padding: 10px 12px; border: 1px solid #e5e7eb; border-radius: 10px; background: #f8fafc; cursor: pointer; }
.tab.active { border-color: #111827; background: #111827; color: white; } .tab.active { border-color: #111827; background: #111827; color: white; }
.form { display: grid; gap: 10px; max-width: 560px; } .form { display: grid; gap: 10px; max-width: 560px; }
.input { padding: 10px 12px; border: 1px solid #e5e7eb; border-radius: 10px; } .input { padding: 10px 12px; border: 1px solid #e5e7eb; border-radius: 10px; }
.btn { padding: 10px 12px; border: 1px solid #111827; border-radius: 10px; background: #111827; color: white; cursor: pointer; } .btn { padding: 10px 12px; border: 1px solid #111827; border-radius: 10px; background: #111827; color: white; cursor: pointer; }
.btn:disabled { opacity: 0.6; cursor: not-allowed; } .btn:disabled { opacity: 0.6; cursor: not-allowed; }
.error { color: #b91c1c; } .error { color: #b91c1c; }
.muted { color: #475569; font-size: 12px; margin-top: 10px; } .muted { color: #475569; font-size: 12px; margin-top: 10px; }
</style> </style>

View File

@@ -1,104 +1,104 @@
<script setup> <script setup>
import { computed, onMounted, ref } from "vue"; import { computed, onMounted, ref } from "vue";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import { getToken, setToken } from "../../lib/extStorage"; import { getToken, setToken } from "../../lib/extStorage";
import BbConfirmModal from "../../components/BbConfirmModal.vue"; import BbConfirmModal from "../../components/BbConfirmModal.vue";
const router = useRouter(); const router = useRouter();
const token = ref(""); const token = ref("");
const loggedIn = computed(() => Boolean(token.value)); const loggedIn = computed(() => Boolean(token.value));
const webBaseUrl = import.meta.env.VITE_WEB_BASE_URL || "http://localhost:5173"; const webBaseUrl = import.meta.env.VITE_WEB_BASE_URL || "http://localhost:5173";
async function refresh() { async function refresh() {
token.value = await getToken(); token.value = await getToken();
} }
function openWeb() { function openWeb() {
const url = String(webBaseUrl || "").trim(); const url = String(webBaseUrl || "").trim();
if (!url) return; if (!url) return;
if (typeof chrome !== "undefined" && chrome.tabs?.create) chrome.tabs.create({ url }); if (typeof chrome !== "undefined" && chrome.tabs?.create) chrome.tabs.create({ url });
else window.open(url, "_blank", "noopener,noreferrer"); else window.open(url, "_blank", "noopener,noreferrer");
} }
const logoutModalOpen = ref(false); const logoutModalOpen = ref(false);
const logoutStep = ref(1); const logoutStep = ref(1);
function startLogout() { function startLogout() {
logoutStep.value = 1; logoutStep.value = 1;
logoutModalOpen.value = true; logoutModalOpen.value = true;
} }
async function confirmLogout() { async function confirmLogout() {
if (logoutStep.value === 1) { if (logoutStep.value === 1) {
logoutStep.value = 2; logoutStep.value = 2;
return; return;
} }
await setToken(""); await setToken("");
logoutModalOpen.value = false; logoutModalOpen.value = false;
await refresh(); await refresh();
await router.replace("/login"); await router.replace("/login");
} }
function cancelLogout() { function cancelLogout() {
logoutModalOpen.value = false; logoutModalOpen.value = false;
logoutStep.value = 1; logoutStep.value = 1;
} }
onMounted(refresh); onMounted(refresh);
</script> </script>
<template> <template>
<section class="page"> <section class="page">
<h1 class="h1">更多操作</h1> <h1 class="h1">更多操作</h1>
<p v-if="!loggedIn" class="muted">当前未登录将跳转到登录页</p> <p v-if="!loggedIn" class="muted">当前未登录将跳转到登录页</p>
<div class="card"> <div class="card">
<button class="btn" type="button" @click="openWeb">跳转 Web</button> <button class="btn" type="button" @click="openWeb">跳转 Web</button>
<button class="btn btn--secondary" type="button" @click="startLogout">退出登录</button> <button class="btn btn--secondary" type="button" @click="startLogout">退出登录</button>
<p class="hint">Web 地址来自环境变量VITE_WEB_BASE_URL</p> <p class="hint">Web 地址来自环境变量VITE_WEB_BASE_URL</p>
</div> </div>
<BbConfirmModal <BbConfirmModal
v-model="logoutModalOpen" v-model="logoutModalOpen"
title="退出登录" title="退出登录"
:message="logoutStep === 1 ? '退出以后无法同步书签。' : '你确定要退出吗?'" :message="logoutStep === 1 ? '退出以后无法同步书签。' : '你确定要退出吗?'"
:confirm-text="logoutStep === 1 ? '继续' : '确定退出'" :confirm-text="logoutStep === 1 ? '继续' : '确定退出'"
cancel-text="取消" cancel-text="取消"
:danger="logoutStep === 2" :danger="logoutStep === 2"
@confirm="confirmLogout" @confirm="confirmLogout"
@cancel="cancelLogout" @cancel="cancelLogout"
/> />
</section> </section>
</template> </template>
<style scoped> <style scoped>
.page { padding: 14px; } .page { padding: 14px; }
.h1 { margin: 0 0 10px; font-size: 18px; } .h1 { margin: 0 0 10px; font-size: 18px; }
.card { .card {
max-width: 560px; max-width: 560px;
border: 1px solid rgba(255,255,255,0.65); border: 1px solid rgba(255,255,255,0.65);
background: var(--bb-card); background: var(--bb-card);
border-radius: 18px; border-radius: 18px;
padding: 12px; padding: 12px;
display: grid; display: grid;
gap: 10px; gap: 10px;
} }
.btn { .btn {
border: 1px solid rgba(255,255,255,0.25); border: 1px solid rgba(255,255,255,0.25);
border-radius: 14px; border-radius: 14px;
padding: 10px 12px; padding: 10px 12px;
background: linear-gradient(135deg, var(--bb-primary), var(--bb-cta)); background: linear-gradient(135deg, var(--bb-primary), var(--bb-cta));
color: white; color: white;
cursor: pointer; cursor: pointer;
} }
.btn--secondary { .btn--secondary {
background: rgba(255,255,255,0.55); background: rgba(255,255,255,0.55);
color: var(--bb-text); color: var(--bb-text);
border-color: var(--bb-border); border-color: var(--bb-border);
} }
.muted { color: rgba(19, 78, 74, 0.72); font-size: 12px; } .muted { color: rgba(19, 78, 74, 0.72); font-size: 12px; }
.hint { color: rgba(19, 78, 74, 0.72); font-size: 12px; margin: 0; } .hint { color: rgba(19, 78, 74, 0.72); font-size: 12px; margin: 0; }
</style> </style>

View File

@@ -1,134 +1,134 @@
<script setup> <script setup>
import { computed, onMounted, ref } from "vue"; import { computed, onMounted, ref } from "vue";
import { apiFetch } from "../../lib/api"; import { apiFetch } from "../../lib/api";
import { getToken } from "../../lib/extStorage"; import { getToken } from "../../lib/extStorage";
import { listLocalBookmarks, markLocalDeleted, upsertLocalBookmark } from "../../lib/localData"; import { listLocalBookmarks, markLocalDeleted, upsertLocalBookmark } from "../../lib/localData";
import BbConfirmModal from "../../components/BbConfirmModal.vue"; import BbConfirmModal from "../../components/BbConfirmModal.vue";
const token = ref(""); const token = ref("");
const loggedIn = computed(() => Boolean(token.value)); const loggedIn = computed(() => Boolean(token.value));
const items = ref([]); const items = ref([]);
const error = ref(""); const error = ref("");
const mode = computed(() => (loggedIn.value ? "cloud" : "local")); const mode = computed(() => (loggedIn.value ? "cloud" : "local"));
const title = ref(""); const title = ref("");
const url = ref(""); const url = ref("");
async function load() { async function load() {
error.value = ""; error.value = "";
try { try {
token.value = await getToken(); token.value = await getToken();
if (!token.value) { if (!token.value) {
items.value = await listLocalBookmarks(); items.value = await listLocalBookmarks();
return; return;
} }
items.value = await apiFetch("/bookmarks"); items.value = await apiFetch("/bookmarks");
} catch (e) { } catch (e) {
error.value = e.message || String(e); error.value = e.message || String(e);
} }
} }
async function add() { async function add() {
if (!title.value || !url.value) return; if (!title.value || !url.value) return;
if (mode.value === "cloud") { if (mode.value === "cloud") {
await apiFetch("/bookmarks", { await apiFetch("/bookmarks", {
method: "POST", method: "POST",
body: JSON.stringify({ folderId: null, title: title.value, url: url.value, visibility: "public" }) body: JSON.stringify({ folderId: null, title: title.value, url: url.value, visibility: "public" })
}); });
} else { } else {
await upsertLocalBookmark({ title: title.value, url: url.value, visibility: "public" }); await upsertLocalBookmark({ title: title.value, url: url.value, visibility: "public" });
} }
title.value = ""; title.value = "";
url.value = ""; url.value = "";
await load(); await load();
} }
async function remove(id) { async function remove(id) {
if (mode.value !== "local") return; if (mode.value !== "local") return;
pendingDeleteId.value = id; pendingDeleteId.value = id;
deleteConfirmOpen.value = true; deleteConfirmOpen.value = true;
} }
const deleteConfirmOpen = ref(false); const deleteConfirmOpen = ref(false);
const pendingDeleteId = ref(""); const pendingDeleteId = ref("");
async function confirmDelete() { async function confirmDelete() {
const id = pendingDeleteId.value; const id = pendingDeleteId.value;
if (!id) { if (!id) {
deleteConfirmOpen.value = false; deleteConfirmOpen.value = false;
return; return;
} }
await markLocalDeleted(id); await markLocalDeleted(id);
pendingDeleteId.value = ""; pendingDeleteId.value = "";
deleteConfirmOpen.value = false; deleteConfirmOpen.value = false;
await load(); await load();
} }
function cancelDelete() { function cancelDelete() {
pendingDeleteId.value = ""; pendingDeleteId.value = "";
deleteConfirmOpen.value = false; deleteConfirmOpen.value = false;
} }
onMounted(load); onMounted(load);
</script> </script>
<template> <template>
<section> <section>
<h1>我的书签{{ mode === 'cloud' ? '云端' : '本地' }}</h1> <h1>我的书签{{ mode === 'cloud' ? '云端' : '本地' }}</h1>
<p v-if="!loggedIn" class="muted">未登录时书签保存在扩展本地可在登录后自动合并上云</p> <p v-if="!loggedIn" class="muted">未登录时书签保存在扩展本地可在登录后自动合并上云</p>
<div class="form"> <div class="form">
<input v-model="title" class="input" placeholder="标题" /> <input v-model="title" class="input" placeholder="标题" />
<input v-model="url" class="input" placeholder="链接" /> <input v-model="url" class="input" placeholder="链接" />
<button class="btn" @click="add">添加</button> <button class="btn" @click="add">添加</button>
</div> </div>
<p v-if="error" class="error">{{ error }}</p> <p v-if="error" class="error">{{ error }}</p>
<ul class="list"> <ul class="list">
<li v-for="b in items" :key="b.id" class="card"> <li v-for="b in items" :key="b.id" class="card">
<div class="row"> <div class="row">
<a :href="b.url" target="_blank" rel="noopener" class="title">{{ b.title }}</a> <a :href="b.url" target="_blank" rel="noopener" class="title">{{ b.title }}</a>
<button v-if="mode === 'local'" class="ghost" @click.prevent="remove(b.id)">删除</button> <button v-if="mode === 'local'" class="ghost" @click.prevent="remove(b.id)">删除</button>
</div> </div>
<div class="muted">{{ b.url }}</div> <div class="muted">{{ b.url }}</div>
</li> </li>
</ul> </ul>
<BbConfirmModal <BbConfirmModal
v-model="deleteConfirmOpen" v-model="deleteConfirmOpen"
title="删除书签" title="删除书签"
message="确定删除该书签?" message="确定删除该书签?"
confirm-text="删除" confirm-text="删除"
cancel-text="取消" cancel-text="取消"
:danger="true" :danger="true"
@confirm="confirmDelete" @confirm="confirmDelete"
@cancel="cancelDelete" @cancel="cancelDelete"
/> />
</section> </section>
</template> </template>
<style scoped> <style scoped>
.form { display: grid; gap: 10px; grid-template-columns: 1fr; margin: 12px 0; } .form { display: grid; gap: 10px; grid-template-columns: 1fr; margin: 12px 0; }
@media (min-width: 900px) { .form { grid-template-columns: 2fr 3fr auto; } } @media (min-width: 900px) { .form { grid-template-columns: 2fr 3fr auto; } }
.input { padding: 10px 12px; border: 1px solid #e5e7eb; border-radius: 10px; } .input { padding: 10px 12px; border: 1px solid #e5e7eb; border-radius: 10px; }
.btn { padding: 10px 12px; border: 1px solid #111827; border-radius: 10px; background: #111827; color: white; cursor: pointer; } .btn { padding: 10px 12px; border: 1px solid #111827; border-radius: 10px; background: #111827; color: white; cursor: pointer; }
.row { display: flex; justify-content: space-between; align-items: start; gap: 10px; } .row { display: flex; justify-content: space-between; align-items: start; gap: 10px; }
.ghost { border: 1px solid #e5e7eb; background: #f8fafc; border-radius: 10px; padding: 6px 10px; cursor: pointer; } .ghost { border: 1px solid #e5e7eb; background: #f8fafc; border-radius: 10px; padding: 6px 10px; cursor: pointer; }
.list { list-style: none; padding: 0; display: grid; grid-template-columns: 1fr; gap: 10px; } .list { list-style: none; padding: 0; display: grid; grid-template-columns: 1fr; gap: 10px; }
@media (min-width: 900px) { .list { grid-template-columns: 1fr 1fr; } } @media (min-width: 900px) { .list { grid-template-columns: 1fr 1fr; } }
.card { border: 1px solid #e5e7eb; border-radius: 14px; padding: 12px; background: white; } .card { border: 1px solid #e5e7eb; border-radius: 14px; padding: 12px; background: white; }
.title { .title {
color: #111827; color: #111827;
font-weight: 700; font-weight: 700;
text-decoration: none; text-decoration: none;
flex: 1; flex: 1;
min-width: 0; min-width: 0;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.muted { color: #475569; font-size: 12px; overflow-wrap: anywhere; margin-top: 6px; } .muted { color: #475569; font-size: 12px; overflow-wrap: anywhere; margin-top: 6px; }
.error { color: #b91c1c; } .error { color: #b91c1c; }
.muted { color: #475569; font-size: 12px; } .muted { color: #475569; font-size: 12px; }
</style> </style>

View File

@@ -1,92 +1,92 @@
<script setup> <script setup>
import { onBeforeUnmount, onMounted, ref, watch } from "vue"; import { onBeforeUnmount, onMounted, ref, watch } from "vue";
import { apiFetch } from "../../lib/api"; import { apiFetch } from "../../lib/api";
const items = ref([]); const items = ref([]);
const q = ref(""); const q = ref("");
const loading = ref(false); const loading = ref(false);
const error = ref(""); const error = ref("");
async function load() { async function load() {
loading.value = true; loading.value = true;
error.value = ""; error.value = "";
try { try {
items.value = await apiFetch(`/bookmarks/public?q=${encodeURIComponent(q.value)}`); items.value = await apiFetch(`/bookmarks/public?q=${encodeURIComponent(q.value)}`);
} catch (e) { } catch (e) {
error.value = e.message || String(e); error.value = e.message || String(e);
} finally { } finally {
loading.value = false; loading.value = false;
} }
} }
let searchTimer = 0; let searchTimer = 0;
watch( watch(
() => q.value, () => q.value,
() => { () => {
window.clearTimeout(searchTimer); window.clearTimeout(searchTimer);
searchTimer = window.setTimeout(() => { searchTimer = window.setTimeout(() => {
load(); load();
}, 200); }, 200);
} }
); );
onBeforeUnmount(() => { onBeforeUnmount(() => {
window.clearTimeout(searchTimer); window.clearTimeout(searchTimer);
}); });
onMounted(load); onMounted(load);
</script> </script>
<template> <template>
<section> <section>
<h1>公开书签</h1> <h1>公开书签</h1>
<div class="row"> <div class="row">
<div class="searchWrap"> <div class="searchWrap">
<input v-model="q" class="input input--withClear" placeholder="搜索" /> <input v-model="q" class="input input--withClear" placeholder="搜索" />
<button v-if="q.trim()" class="clearBtn" type="button" aria-label="清空搜索" @click="q = ''">×</button> <button v-if="q.trim()" class="clearBtn" type="button" aria-label="清空搜索" @click="q = ''">×</button>
</div> </div>
</div> </div>
<p v-if="error" class="error">{{ error }}</p> <p v-if="error" class="error">{{ error }}</p>
<ul class="list"> <ul class="list">
<li v-for="b in items" :key="b.id" class="card"> <li v-for="b in items" :key="b.id" class="card">
<a :href="b.url" target="_blank" rel="noopener" class="title">{{ b.title }}</a> <a :href="b.url" target="_blank" rel="noopener" class="title">{{ b.title }}</a>
<div class="muted">{{ b.url }}</div> <div class="muted">{{ b.url }}</div>
</li> </li>
</ul> </ul>
</section> </section>
</template> </template>
<style scoped> <style scoped>
.row { display: flex; gap: 8px; margin: 12px 0; } .row { display: flex; gap: 8px; margin: 12px 0; }
.searchWrap { flex: 1; position: relative; } .searchWrap { flex: 1; position: relative; }
.input { width: 100%; padding: 10px 12px; border: 1px solid #e5e7eb; border-radius: 10px; } .input { width: 100%; padding: 10px 12px; border: 1px solid #e5e7eb; border-radius: 10px; }
.input.input--withClear { padding-right: 40px; } .input.input--withClear { padding-right: 40px; }
.clearBtn { .clearBtn {
position: absolute; position: absolute;
right: 10px; right: 10px;
top: 50%; top: 50%;
transform: translateY(-50%); transform: translateY(-50%);
width: 28px; width: 28px;
height: 28px; height: 28px;
border-radius: 999px; border-radius: 999px;
border: 1px solid #e5e7eb; border: 1px solid #e5e7eb;
background: white; background: white;
cursor: pointer; cursor: pointer;
} }
.btn { padding: 10px 12px; border: 1px solid #111827; border-radius: 10px; background: #111827; color: white; cursor: pointer; } .btn { padding: 10px 12px; border: 1px solid #111827; border-radius: 10px; background: #111827; color: white; cursor: pointer; }
.btn:disabled { opacity: 0.6; cursor: not-allowed; } .btn:disabled { opacity: 0.6; cursor: not-allowed; }
.list { list-style: none; padding: 0; display: grid; grid-template-columns: 1fr; gap: 10px; } .list { list-style: none; padding: 0; display: grid; grid-template-columns: 1fr; gap: 10px; }
@media (min-width: 900px) { .list { grid-template-columns: 1fr 1fr; } } @media (min-width: 900px) { .list { grid-template-columns: 1fr 1fr; } }
.card { border: 1px solid #e5e7eb; border-radius: 14px; padding: 12px; background: white; } .card { border: 1px solid #e5e7eb; border-radius: 14px; padding: 12px; background: white; }
.title { .title {
color: #111827; color: #111827;
font-weight: 700; font-weight: 700;
text-decoration: none; text-decoration: none;
display: block; display: block;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.muted { color: #475569; font-size: 12px; overflow-wrap: anywhere; margin-top: 6px; } .muted { color: #475569; font-size: 12px; overflow-wrap: anywhere; margin-top: 6px; }
.error { color: #b91c1c; } .error { color: #b91c1c; }
</style> </style>

View File

@@ -1,22 +1,22 @@
import { createRouter, createWebHashHistory } from "vue-router"; import { createRouter, createWebHashHistory } from "vue-router";
import LoginPage from "./pages/LoginPage.vue"; import LoginPage from "./pages/LoginPage.vue";
import MorePage from "./pages/MorePage.vue"; import MorePage from "./pages/MorePage.vue";
import { getToken } from "../lib/extStorage"; import { getToken } from "../lib/extStorage";
export const router = createRouter({ export const router = createRouter({
history: createWebHashHistory(), history: createWebHashHistory(),
routes: [ routes: [
{ path: "/", component: MorePage }, { path: "/", component: MorePage },
{ path: "/login", component: LoginPage } { path: "/login", component: LoginPage }
] ]
}); });
router.beforeEach(async (to) => { router.beforeEach(async (to) => {
const token = await getToken(); const token = await getToken();
const authed = Boolean(token); const authed = Boolean(token);
if (!authed && to.path !== "/login") return "/login"; if (!authed && to.path !== "/login") return "/login";
if (authed && to.path === "/login") return "/"; if (authed && to.path === "/login") return "/";
return true; return true;
}); });

View File

@@ -1,494 +1,494 @@
<script setup> <script setup>
import { computed, onMounted, ref, watch } from "vue"; import { computed, onMounted, ref, watch } from "vue";
import { apiFetch } from "../lib/api"; import { apiFetch } from "../lib/api";
import { getToken } from "../lib/extStorage"; import { getToken } from "../lib/extStorage";
const view = ref("list"); // add | list const view = ref("list"); // add | list
const token = ref(""); const token = ref("");
const loggedIn = computed(() => Boolean(token.value)); const loggedIn = computed(() => Boolean(token.value));
const loading = ref(false); const loading = ref(false);
const error = ref(""); const error = ref("");
function openOptions() { function openOptions() {
if (typeof chrome !== "undefined" && chrome.runtime?.openOptionsPage) { if (typeof chrome !== "undefined" && chrome.runtime?.openOptionsPage) {
chrome.runtime.openOptionsPage(); chrome.runtime.openOptionsPage();
} }
} }
function openUrl(url) { function openUrl(url) {
if (!url) return; if (!url) return;
if (typeof chrome !== "undefined" && chrome.tabs?.create) { if (typeof chrome !== "undefined" && chrome.tabs?.create) {
chrome.tabs.create({ url }); chrome.tabs.create({ url });
} else { } else {
window.open(url, "_blank", "noopener,noreferrer"); window.open(url, "_blank", "noopener,noreferrer");
} }
} }
async function refreshAuth() { async function refreshAuth() {
token.value = await getToken(); token.value = await getToken();
} }
// folders + bookmarks // folders + bookmarks
const q = ref(""); const q = ref("");
const folders = ref([]); const folders = ref([]);
const items = ref([]); const items = ref([]);
const openFolderIds = ref(new Set()); const openFolderIds = ref(new Set());
const bookmarksByFolderId = computed(() => { const bookmarksByFolderId = computed(() => {
const map = new Map(); const map = new Map();
for (const b of items.value || []) { for (const b of items.value || []) {
const key = b.folderId ?? null; const key = b.folderId ?? null;
if (!map.has(key)) map.set(key, []); if (!map.has(key)) map.set(key, []);
map.get(key).push(b); map.get(key).push(b);
} }
return map; return map;
}); });
function folderCount(folderId) { function folderCount(folderId) {
return (bookmarksByFolderId.value.get(folderId ?? null) || []).length; return (bookmarksByFolderId.value.get(folderId ?? null) || []).length;
} }
function toggleFolder(folderId) { function toggleFolder(folderId) {
const next = new Set(openFolderIds.value); const next = new Set(openFolderIds.value);
if (next.has(folderId)) next.delete(folderId); if (next.has(folderId)) next.delete(folderId);
else next.add(folderId); else next.add(folderId);
openFolderIds.value = next; openFolderIds.value = next;
} }
function isFolderOpen(folderId) { function isFolderOpen(folderId) {
if (q.value.trim()) return true; if (q.value.trim()) return true;
return openFolderIds.value.has(folderId); return openFolderIds.value.has(folderId);
} }
async function loadFolders() { async function loadFolders() {
const list = await apiFetch("/folders"); const list = await apiFetch("/folders");
folders.value = Array.isArray(list) ? list : []; folders.value = Array.isArray(list) ? list : [];
} }
async function loadBookmarks() { async function loadBookmarks() {
const qs = q.value.trim() ? `?q=${encodeURIComponent(q.value.trim())}` : ""; const qs = q.value.trim() ? `?q=${encodeURIComponent(q.value.trim())}` : "";
const list = await apiFetch(`/bookmarks${qs}`); const list = await apiFetch(`/bookmarks${qs}`);
items.value = Array.isArray(list) ? list : []; items.value = Array.isArray(list) ? list : [];
} }
async function loadAll() { async function loadAll() {
if (!loggedIn.value) return; if (!loggedIn.value) return;
loading.value = true; loading.value = true;
error.value = ""; error.value = "";
try { try {
await Promise.all([loadFolders(), loadBookmarks()]); await Promise.all([loadFolders(), loadBookmarks()]);
} catch (e) { } catch (e) {
error.value = e.message || String(e); error.value = e.message || String(e);
} finally { } finally {
loading.value = false; loading.value = false;
} }
} }
// add current page // add current page
const addBusy = ref(false); const addBusy = ref(false);
const addStatus = ref(""); const addStatus = ref("");
const addFolderId = ref(null); const addFolderId = ref(null);
const addTitle = ref(""); const addTitle = ref("");
const addUrl = ref(""); const addUrl = ref("");
async function getActiveTabPage() { async function getActiveTabPage() {
try { try {
if (typeof chrome !== "undefined" && chrome.tabs?.query) { if (typeof chrome !== "undefined" && chrome.tabs?.query) {
const tabs = await new Promise((resolve) => { const tabs = await new Promise((resolve) => {
chrome.tabs.query({ active: true, currentWindow: true }, (result) => resolve(result || [])); chrome.tabs.query({ active: true, currentWindow: true }, (result) => resolve(result || []));
}); });
const tab = tabs?.[0]; const tab = tabs?.[0];
return { title: tab?.title || "", url: tab?.url || "" }; return { title: tab?.title || "", url: tab?.url || "" };
} }
} catch { } catch {
// ignore // ignore
} }
return { title: "", url: "" }; return { title: "", url: "" };
} }
async function prepareAddCurrent() { async function prepareAddCurrent() {
addStatus.value = ""; addStatus.value = "";
error.value = ""; error.value = "";
addFolderId.value = null; addFolderId.value = null;
const page = await getActiveTabPage(); const page = await getActiveTabPage();
addTitle.value = String(page.title || "").trim() || String(page.url || "").trim(); addTitle.value = String(page.title || "").trim() || String(page.url || "").trim();
addUrl.value = String(page.url || "").trim(); addUrl.value = String(page.url || "").trim();
if (loggedIn.value) { if (loggedIn.value) {
await loadFolders().catch(() => {}); await loadFolders().catch(() => {});
} }
} }
async function submitAddCurrent() { async function submitAddCurrent() {
addStatus.value = ""; addStatus.value = "";
error.value = ""; error.value = "";
if (!loggedIn.value) { if (!loggedIn.value) {
error.value = "请先在『更多操作』里登录"; error.value = "请先在『更多操作』里登录";
return; return;
} }
const t = addTitle.value.trim() || addUrl.value.trim(); const t = addTitle.value.trim() || addUrl.value.trim();
const u = addUrl.value.trim(); const u = addUrl.value.trim();
if (!u) return; if (!u) return;
try { try {
addBusy.value = true; addBusy.value = true;
await apiFetch("/bookmarks", { await apiFetch("/bookmarks", {
method: "POST", method: "POST",
body: JSON.stringify({ body: JSON.stringify({
folderId: addFolderId.value ?? null, folderId: addFolderId.value ?? null,
title: t, title: t,
url: u, url: u,
visibility: "private" visibility: "private"
}) })
}); });
addStatus.value = "已添加"; addStatus.value = "已添加";
if (view.value === "list") await loadBookmarks(); if (view.value === "list") await loadBookmarks();
} catch (e) { } catch (e) {
error.value = e.message || String(e); error.value = e.message || String(e);
} finally { } finally {
addBusy.value = false; addBusy.value = false;
} }
} }
// create folder (cloud only) // create folder (cloud only)
const folderName = ref(""); const folderName = ref("");
const folderBusy = ref(false); const folderBusy = ref(false);
const folderModalOpen = ref(false); const folderModalOpen = ref(false);
async function createFolder() { async function createFolder() {
error.value = ""; error.value = "";
const name = folderName.value.trim(); const name = folderName.value.trim();
if (!name) return; if (!name) return;
if (!loggedIn.value) { if (!loggedIn.value) {
error.value = "请先登录"; error.value = "请先登录";
return; return;
} }
try { try {
folderBusy.value = true; folderBusy.value = true;
await apiFetch("/folders", { await apiFetch("/folders", {
method: "POST", method: "POST",
body: JSON.stringify({ parentId: null, name, visibility: "private" }) body: JSON.stringify({ parentId: null, name, visibility: "private" })
}); });
folderName.value = ""; folderName.value = "";
folderModalOpen.value = false; folderModalOpen.value = false;
await loadFolders(); await loadFolders();
} catch (e) { } catch (e) {
error.value = e.message || String(e); error.value = e.message || String(e);
} finally { } finally {
folderBusy.value = false; folderBusy.value = false;
} }
} }
onMounted(async () => { onMounted(async () => {
await refreshAuth(); await refreshAuth();
await prepareAddCurrent(); await prepareAddCurrent();
if (loggedIn.value) await loadAll(); if (loggedIn.value) await loadAll();
}); });
watch( watch(
() => q.value, () => q.value,
async () => { async () => {
if (!loggedIn.value) return; if (!loggedIn.value) return;
await loadBookmarks(); await loadBookmarks();
} }
); );
</script> </script>
<template> <template>
<div class="wrap"> <div class="wrap">
<header class="top"> <header class="top">
<div class="brand">云书签</div> <div class="brand">云书签</div>
<button class="btn btn--secondary" type="button" @click="openOptions">更多操作</button> <button class="btn btn--secondary" type="button" @click="openOptions">更多操作</button>
</header> </header>
<div class="seg"> <div class="seg">
<button class="segBtn" :class="view === 'add' ? 'is-active' : ''" type="button" @click="view = 'add'"> <button class="segBtn" :class="view === 'add' ? 'is-active' : ''" type="button" @click="view = 'add'">
一键添加书签 一键添加书签
</button> </button>
<button class="segBtn" :class="view === 'list' ? 'is-active' : ''" type="button" @click="view = 'list'"> <button class="segBtn" :class="view === 'list' ? 'is-active' : ''" type="button" @click="view = 'list'">
书签目录 书签目录
</button> </button>
</div> </div>
<p v-if="!loggedIn" class="hint">未登录请点右上角更多操作先登录</p> <p v-if="!loggedIn" class="hint">未登录请点右上角更多操作先登录</p>
<p v-if="error" class="alert">{{ error }}</p> <p v-if="error" class="alert">{{ error }}</p>
<p v-if="addStatus" class="ok">{{ addStatus }}</p> <p v-if="addStatus" class="ok">{{ addStatus }}</p>
<section v-if="view === 'add'" class="card"> <section v-if="view === 'add'" class="card">
<div class="cardTitle">一键添加书签</div> <div class="cardTitle">一键添加书签</div>
<div class="muted">会自动读取标题和链接你也可以手动修改</div> <div class="muted">会自动读取标题和链接你也可以手动修改</div>
<label class="label">标题</label> <label class="label">标题</label>
<input v-model="addTitle" class="input" placeholder="标题" /> <input v-model="addTitle" class="input" placeholder="标题" />
<label class="label">链接</label> <label class="label">链接</label>
<input v-model="addUrl" class="input" placeholder="https://..." /> <input v-model="addUrl" class="input" placeholder="https://..." />
<label class="label">文件夹不选则未分组</label> <label class="label">文件夹不选则未分组</label>
<select v-model="addFolderId" class="input" :disabled="!loggedIn"> <select v-model="addFolderId" class="input" :disabled="!loggedIn">
<option :value="null">未分组</option> <option :value="null">未分组</option>
<option v-for="f in folders" :key="f.id" :value="f.id">{{ f.name }}</option> <option v-for="f in folders" :key="f.id" :value="f.id">{{ f.name }}</option>
</select> </select>
<div class="row"> <div class="row">
<button class="btn btn--secondary" type="button" @click="prepareAddCurrent" :disabled="addBusy"> <button class="btn btn--secondary" type="button" @click="prepareAddCurrent" :disabled="addBusy">
重新读取 重新读取
</button> </button>
<button class="btn" type="button" @click="submitAddCurrent" :disabled="addBusy || !addUrl"> <button class="btn" type="button" @click="submitAddCurrent" :disabled="addBusy || !addUrl">
{{ addBusy ? '添加中…' : '添加' }} {{ addBusy ? '添加中…' : '添加' }}
</button> </button>
</div> </div>
</section> </section>
<section v-else class="card"> <section v-else class="card">
<div class="titleRow"> <div class="titleRow">
<div class="cardTitle">书签目录</div> <div class="cardTitle">书签目录</div>
<button class="miniBtn" type="button" :disabled="!loggedIn" @click="folderModalOpen = true">新增文件夹</button> <button class="miniBtn" type="button" :disabled="!loggedIn" @click="folderModalOpen = true">新增文件夹</button>
</div> </div>
<div class="row" style="margin-top: 8px;"> <div class="row" style="margin-top: 8px;">
<div class="searchWrap"> <div class="searchWrap">
<input v-model="q" class="input input--withClear" placeholder="搜索标题/链接" :disabled="!loggedIn" /> <input v-model="q" class="input input--withClear" placeholder="搜索标题/链接" :disabled="!loggedIn" />
<button <button
v-if="q.trim()" v-if="q.trim()"
class="clearBtn" class="clearBtn"
type="button" type="button"
aria-label="清空搜索" aria-label="清空搜索"
@click="q = ''" @click="q = ''"
> >
× ×
</button> </button>
</div> </div>
<button class="btn btn--secondary" type="button" @click="loadAll" :disabled="!loggedIn || loading">刷新</button> <button class="btn btn--secondary" type="button" @click="loadAll" :disabled="!loggedIn || loading">刷新</button>
</div> </div>
<div v-if="folderModalOpen" class="modal" @click.self="folderModalOpen = false"> <div v-if="folderModalOpen" class="modal" @click.self="folderModalOpen = false">
<div class="dialog" role="dialog" aria-modal="true" aria-label="新增文件夹"> <div class="dialog" role="dialog" aria-modal="true" aria-label="新增文件夹">
<div class="dialogTitle">新增文件夹</div> <div class="dialogTitle">新增文件夹</div>
<input <input
v-model="folderName" v-model="folderName"
class="input" class="input"
placeholder="文件夹名称" placeholder="文件夹名称"
:disabled="!loggedIn || folderBusy" :disabled="!loggedIn || folderBusy"
@keyup.enter="createFolder" @keyup.enter="createFolder"
/> />
<div class="dialogActions"> <div class="dialogActions">
<button class="btn btn--secondary" type="button" @click="folderModalOpen = false" :disabled="folderBusy">取消</button> <button class="btn btn--secondary" type="button" @click="folderModalOpen = false" :disabled="folderBusy">取消</button>
<button class="btn" type="button" @click="createFolder" :disabled="!loggedIn || folderBusy"> <button class="btn" type="button" @click="createFolder" :disabled="!loggedIn || folderBusy">
{{ folderBusy ? '创建中…' : '创建' }} {{ folderBusy ? '创建中…' : '创建' }}
</button> </button>
</div> </div>
</div> </div>
</div> </div>
<div v-if="loading" class="muted" style="margin-top: 10px;">加载中</div> <div v-if="loading" class="muted" style="margin-top: 10px;">加载中</div>
<div v-if="loggedIn" class="tree"> <div v-if="loggedIn" class="tree">
<div class="folder" :class="isFolderOpen('ROOT') ? 'is-open' : ''"> <div class="folder" :class="isFolderOpen('ROOT') ? 'is-open' : ''">
<button class="folderHeader" type="button" @click="toggleFolder('ROOT')"> <button class="folderHeader" type="button" @click="toggleFolder('ROOT')">
<span class="folderName">未分组</span> <span class="folderName">未分组</span>
<span class="folderMeta">{{ folderCount(null) }} </span> <span class="folderMeta">{{ folderCount(null) }} </span>
</button> </button>
<div v-if="isFolderOpen('ROOT')" class="folderBody"> <div v-if="isFolderOpen('ROOT')" class="folderBody">
<button <button
v-for="b in bookmarksByFolderId.get(null) || []" v-for="b in bookmarksByFolderId.get(null) || []"
:key="b.id" :key="b.id"
type="button" type="button"
class="bm" class="bm"
@click="openUrl(b.url)" @click="openUrl(b.url)"
> >
<div class="bmTitle">{{ b.title || b.url }}</div> <div class="bmTitle">{{ b.title || b.url }}</div>
<div class="bmUrl">{{ b.url }}</div> <div class="bmUrl">{{ b.url }}</div>
</button> </button>
</div> </div>
</div> </div>
<div v-for="f in folders" :key="f.id" class="folder" :class="isFolderOpen(f.id) ? 'is-open' : ''"> <div v-for="f in folders" :key="f.id" class="folder" :class="isFolderOpen(f.id) ? 'is-open' : ''">
<button class="folderHeader" type="button" @click="toggleFolder(f.id)"> <button class="folderHeader" type="button" @click="toggleFolder(f.id)">
<span class="folderName">{{ f.name }}</span> <span class="folderName">{{ f.name }}</span>
<span class="folderMeta">{{ folderCount(f.id) }} </span> <span class="folderMeta">{{ folderCount(f.id) }} </span>
</button> </button>
<div v-if="isFolderOpen(f.id)" class="folderBody"> <div v-if="isFolderOpen(f.id)" class="folderBody">
<button <button
v-for="b in bookmarksByFolderId.get(f.id) || []" v-for="b in bookmarksByFolderId.get(f.id) || []"
:key="b.id" :key="b.id"
type="button" type="button"
class="bm" class="bm"
@click="openUrl(b.url)" @click="openUrl(b.url)"
> >
<div class="bmTitle">{{ b.title || b.url }}</div> <div class="bmTitle">{{ b.title || b.url }}</div>
<div class="bmUrl">{{ b.url }}</div> <div class="bmUrl">{{ b.url }}</div>
</button> </button>
</div> </div>
</div> </div>
</div> </div>
</section> </section>
</div> </div>
</template> </template>
<style scoped> <style scoped>
.wrap{ .wrap{
width: 380px; width: 380px;
padding: 12px; padding: 12px;
font-family: ui-sans-serif, system-ui; font-family: ui-sans-serif, system-ui;
color: var(--bb-text); color: var(--bb-text);
} }
.top{ display:flex; justify-content:space-between; align-items:center; gap:10px; } .top{ display:flex; justify-content:space-between; align-items:center; gap:10px; }
.brand{ font-weight: 900; letter-spacing: 0.5px; } .brand{ font-weight: 900; letter-spacing: 0.5px; }
.seg{ display:flex; gap:8px; margin-top: 10px; } .seg{ display:flex; gap:8px; margin-top: 10px; }
.segBtn{ .segBtn{
flex:1; flex:1;
border: 1px solid var(--bb-border); border: 1px solid var(--bb-border);
background: rgba(255,255,255,0.85); background: rgba(255,255,255,0.85);
padding: 8px 10px; padding: 8px 10px;
border-radius: 14px; border-radius: 14px;
cursor:pointer; cursor:pointer;
} }
.segBtn.is-active{ .segBtn.is-active{
background: linear-gradient(135deg, var(--bb-primary), var(--bb-cta)); background: linear-gradient(135deg, var(--bb-primary), var(--bb-cta));
color: white; color: white;
border-color: rgba(255,255,255,0.35); border-color: rgba(255,255,255,0.35);
} }
.btn{ .btn{
border: 1px solid rgba(15, 23, 42, 0.10); border: 1px solid rgba(15, 23, 42, 0.10);
border-radius: 14px; border-radius: 14px;
padding: 8px 12px; padding: 8px 12px;
background: linear-gradient(135deg, var(--bb-primary), var(--bb-cta)); background: linear-gradient(135deg, var(--bb-primary), var(--bb-cta));
color: white; color: white;
cursor: pointer; cursor: pointer;
box-shadow: 0 6px 16px rgba(15, 23, 42, 0.12); box-shadow: 0 6px 16px rgba(15, 23, 42, 0.12);
} }
.btn--secondary{ .btn--secondary{
background: rgba(255,255,255,0.92); background: rgba(255,255,255,0.92);
color: var(--bb-text); color: var(--bb-text);
border-color: var(--bb-border); border-color: var(--bb-border);
box-shadow: none; box-shadow: none;
} }
button:disabled{ opacity: 0.6; cursor: not-allowed; } button:disabled{ opacity: 0.6; cursor: not-allowed; }
.hint{ margin: 10px 0 0; color: var(--bb-muted); font-size: 12px; } .hint{ margin: 10px 0 0; color: var(--bb-muted); font-size: 12px; }
.alert{ margin: 10px 0 0; padding: 8px 10px; border-radius: 12px; border: 1px solid rgba(248,113,113,0.35); background: rgba(248,113,113,0.08); color: #7f1d1d; font-size: 12px; } .alert{ margin: 10px 0 0; padding: 8px 10px; border-radius: 12px; border: 1px solid rgba(248,113,113,0.35); background: rgba(248,113,113,0.08); color: #7f1d1d; font-size: 12px; }
.ok{ margin: 10px 0 0; padding: 8px 10px; border-radius: 12px; border: 1px solid rgba(34,197,94,0.35); background: rgba(34,197,94,0.10); color: #14532d; font-size: 12px; } .ok{ margin: 10px 0 0; padding: 8px 10px; border-radius: 12px; border: 1px solid rgba(34,197,94,0.35); background: rgba(34,197,94,0.10); color: #14532d; font-size: 12px; }
.card{ .card{
margin-top: 10px; margin-top: 10px;
border: 1px solid rgba(255,255,255,0.65); border: 1px solid rgba(255,255,255,0.65);
background: var(--bb-card); background: var(--bb-card);
border-radius: 18px; border-radius: 18px;
padding: 12px; padding: 12px;
backdrop-filter: blur(10px); backdrop-filter: blur(10px);
box-shadow: 0 12px 28px rgba(15, 23, 42, 0.10); box-shadow: 0 12px 28px rgba(15, 23, 42, 0.10);
} }
.cardTitle{ font-weight: 900; } .cardTitle{ font-weight: 900; }
.muted{ color: var(--bb-muted); font-size: 12px; margin-top: 4px; } .muted{ color: var(--bb-muted); font-size: 12px; margin-top: 4px; }
.label{ display:block; margin-top: 10px; font-size: 12px; color: rgba(19, 78, 74, 0.72); } .label{ display:block; margin-top: 10px; font-size: 12px; color: rgba(19, 78, 74, 0.72); }
.input{ .input{
width: 100%; width: 100%;
margin-top: 6px; margin-top: 6px;
padding: 8px 10px; padding: 8px 10px;
border-radius: 14px; border-radius: 14px;
border: 1px solid var(--bb-border); border: 1px solid var(--bb-border);
background: rgba(255,255,255,0.92); background: rgba(255,255,255,0.92);
} }
.searchWrap{ position: relative; flex: 1; min-width: 0; } .searchWrap{ position: relative; flex: 1; min-width: 0; }
.input.input--withClear{ padding-right: 40px; } .input.input--withClear{ padding-right: 40px; }
.clearBtn{ .clearBtn{
position: absolute; position: absolute;
right: 10px; right: 10px;
top: 50%; top: 50%;
transform: translateY(-50%); transform: translateY(-50%);
width: 26px; width: 26px;
height: 26px; height: 26px;
border-radius: 999px; border-radius: 999px;
border: 1px solid var(--bb-border); border: 1px solid var(--bb-border);
background: rgba(255,255,255,0.92); background: rgba(255,255,255,0.92);
cursor: pointer; cursor: pointer;
display: grid; display: grid;
place-items: center; place-items: center;
color: rgba(15, 23, 42, 0.7); color: rgba(15, 23, 42, 0.7);
padding: 0; padding: 0;
} }
.clearBtn:hover{ background: rgba(255,255,255,1); } .clearBtn:hover{ background: rgba(255,255,255,1); }
.row{ display:flex; gap: 8px; align-items:center; margin-top: 10px; } .row{ display:flex; gap: 8px; align-items:center; margin-top: 10px; }
.row .input{ margin-top: 0; } .row .input{ margin-top: 0; }
.subCard{ margin-top: 10px; padding: 10px; border-radius: 16px; border: 1px dashed rgba(19,78,74,0.22); background: rgba(255,255,255,0.55); } .subCard{ margin-top: 10px; padding: 10px; border-radius: 16px; border: 1px dashed rgba(19,78,74,0.22); background: rgba(255,255,255,0.55); }
.subTitle{ font-weight: 800; } .subTitle{ font-weight: 800; }
.titleRow{ display:flex; align-items:center; justify-content:space-between; gap:10px; } .titleRow{ display:flex; align-items:center; justify-content:space-between; gap:10px; }
.miniBtn{ .miniBtn{
padding: 6px 10px; padding: 6px 10px;
border-radius: 12px; border-radius: 12px;
border: 1px solid var(--bb-border); border: 1px solid var(--bb-border);
background: rgba(255,255,255,0.92); background: rgba(255,255,255,0.92);
cursor: pointer; cursor: pointer;
font-size: 12px; font-size: 12px;
font-weight: 700; font-weight: 700;
box-shadow: none; box-shadow: none;
} }
.miniBtn:disabled{ opacity: 0.6; cursor: not-allowed; } .miniBtn:disabled{ opacity: 0.6; cursor: not-allowed; }
.modal{ .modal{
position: fixed; position: fixed;
inset: 0; inset: 0;
background: rgba(15, 23, 42, 0.35); background: rgba(15, 23, 42, 0.35);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
padding: 14px; padding: 14px;
z-index: 50; z-index: 50;
} }
.dialog{ .dialog{
width: 100%; width: 100%;
max-width: 340px; max-width: 340px;
border-radius: 18px; border-radius: 18px;
border: 1px solid rgba(255,255,255,0.65); border: 1px solid rgba(255,255,255,0.65);
background: var(--bb-card-solid); background: var(--bb-card-solid);
box-shadow: 0 18px 40px rgba(15, 23, 42, 0.25); box-shadow: 0 18px 40px rgba(15, 23, 42, 0.25);
padding: 12px; padding: 12px;
} }
.dialogTitle{ font-weight: 900; margin-bottom: 8px; } .dialogTitle{ font-weight: 900; margin-bottom: 8px; }
.dialogActions{ display:flex; justify-content:flex-end; gap: 8px; margin-top: 10px; } .dialogActions{ display:flex; justify-content:flex-end; gap: 8px; margin-top: 10px; }
.tree{ margin-top: 10px; display: grid; gap: 10px; } .tree{ margin-top: 10px; display: grid; gap: 10px; }
.folder{ border: 1px solid rgba(255,255,255,0.55); border-radius: 16px; background: rgba(255,255,255,0.55); } .folder{ border: 1px solid rgba(255,255,255,0.55); border-radius: 16px; background: rgba(255,255,255,0.55); }
.folderHeader{ .folderHeader{
width: 100%; width: 100%;
text-align: left; text-align: left;
padding: 8px 10px; padding: 8px 10px;
border: 0; border: 0;
background: transparent; background: transparent;
cursor: pointer; cursor: pointer;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
gap: 10px; gap: 10px;
} }
.folderName{ font-weight: 900; flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .folderName{ font-weight: 900; flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.folderMeta{ font-size: 12px; color: rgba(19, 78, 74, 0.72); } .folderMeta{ font-size: 12px; color: rgba(19, 78, 74, 0.72); }
.folderBody{ padding: 8px 10px 10px; display: grid; gap: 8px; } .folderBody{ padding: 8px 10px 10px; display: grid; gap: 8px; }
.bm{ .bm{
border: 1px solid rgba(255,255,255,0.65); border: 1px solid rgba(255,255,255,0.65);
background: rgba(255,255,255,0.92); background: rgba(255,255,255,0.92);
border-radius: 14px; border-radius: 14px;
padding: 8px 10px; padding: 8px 10px;
cursor: pointer; cursor: pointer;
text-align: left; text-align: left;
} }
.bmTitle{ font-weight: 800; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .bmTitle{ font-weight: 800; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.bmUrl{ font-size: 12px; color: rgba(19, 78, 74, 0.72); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-top: 4px; } .bmUrl{ font-size: 12px; color: rgba(19, 78, 74, 0.72); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-top: 4px; }
</style> </style>

View File

@@ -1,6 +1,6 @@
import { createApp } from "vue"; import { createApp } from "vue";
import PopupApp from "./PopupApp.vue"; import PopupApp from "./PopupApp.vue";
import "../style.css"; import "../style.css";
createApp(PopupApp).mount("#app"); createApp(PopupApp).mount("#app");

View File

@@ -1,12 +1,12 @@
export default [ export default [
{ {
files: ["**/*.js"], files: ["**/*.js"],
languageOptions: { languageOptions: {
ecmaVersion: 2024, ecmaVersion: 2024,
sourceType: "module" sourceType: "module"
}, },
rules: { rules: {
"no-unused-vars": ["error", { "argsIgnorePattern": "^_" }] "no-unused-vars": ["error", { "argsIgnorePattern": "^_" }]
} }
} }
]; ];

View File

@@ -1,44 +1,44 @@
create extension if not exists pgcrypto; create extension if not exists pgcrypto;
create table if not exists users ( create table if not exists users (
id uuid primary key default gen_random_uuid(), id uuid primary key default gen_random_uuid(),
email text not null unique, email text not null unique,
password_hash text not null, password_hash text not null,
role text not null default 'user', role text not null default 'user',
created_at timestamptz not null default now(), created_at timestamptz not null default now(),
updated_at timestamptz not null default now() updated_at timestamptz not null default now()
); );
create table if not exists bookmark_folders ( create table if not exists bookmark_folders (
id uuid primary key default gen_random_uuid(), id uuid primary key default gen_random_uuid(),
user_id uuid not null references users(id) on delete cascade, user_id uuid not null references users(id) on delete cascade,
parent_id uuid null references bookmark_folders(id) on delete cascade, parent_id uuid null references bookmark_folders(id) on delete cascade,
name text not null, name text not null,
visibility text not null default 'private', visibility text not null default 'private',
sort_order integer not null default 0, sort_order integer not null default 0,
created_at timestamptz not null default now(), created_at timestamptz not null default now(),
updated_at timestamptz not null default now() updated_at timestamptz not null default now()
); );
create index if not exists idx_bookmark_folders_user_parent on bookmark_folders (user_id, parent_id); create index if not exists idx_bookmark_folders_user_parent on bookmark_folders (user_id, parent_id);
create table if not exists bookmarks ( create table if not exists bookmarks (
id uuid primary key default gen_random_uuid(), id uuid primary key default gen_random_uuid(),
user_id uuid not null references users(id) on delete cascade, user_id uuid not null references users(id) on delete cascade,
folder_id uuid null references bookmark_folders(id) on delete set null, folder_id uuid null references bookmark_folders(id) on delete set null,
sort_order integer not null default 0, sort_order integer not null default 0,
title text not null, title text not null,
url text not null, url text not null,
url_normalized text not null, url_normalized text not null,
url_hash text not null, url_hash text not null,
visibility text not null default 'private', visibility text not null default 'private',
source text not null default 'manual', source text not null default 'manual',
created_at timestamptz not null default now(), created_at timestamptz not null default now(),
updated_at timestamptz not null default now(), updated_at timestamptz not null default now(),
deleted_at timestamptz null deleted_at timestamptz null
); );
create index if not exists idx_bookmarks_user_updated_at on bookmarks (user_id, updated_at); create index if not exists idx_bookmarks_user_updated_at on bookmarks (user_id, updated_at);
create index if not exists idx_bookmarks_user_folder_sort on bookmarks (user_id, folder_id, sort_order); create index if not exists idx_bookmarks_user_folder_sort on bookmarks (user_id, folder_id, sort_order);
create index if not exists idx_bookmarks_user_url_hash on bookmarks (user_id, url_hash); create index if not exists idx_bookmarks_user_url_hash on bookmarks (user_id, url_hash);
create index if not exists idx_bookmarks_visibility on bookmarks (visibility); create index if not exists idx_bookmarks_visibility on bookmarks (visibility);

View File

@@ -1,5 +1,5 @@
alter table if exists bookmark_folders alter table if exists bookmark_folders
add column if not exists sort_order integer not null default 0; add column if not exists sort_order integer not null default 0;
create index if not exists idx_bookmark_folders_user_parent_sort create index if not exists idx_bookmark_folders_user_parent_sort
on bookmark_folders (user_id, parent_id, sort_order); on bookmark_folders (user_id, parent_id, sort_order);

View File

@@ -1,5 +1,5 @@
alter table if exists bookmarks alter table if exists bookmarks
add column if not exists sort_order integer not null default 0; add column if not exists sort_order integer not null default 0;
create index if not exists idx_bookmarks_user_folder_sort create index if not exists idx_bookmarks_user_folder_sort
on bookmarks (user_id, folder_id, sort_order); on bookmarks (user_id, folder_id, sort_order);

View File

@@ -1,33 +1,33 @@
{ {
"name": "@browser-bookmark/server", "name": "@browser-bookmark/server",
"private": true, "private": true,
"type": "module", "type": "module",
"version": "0.1.0", "version": "0.1.0",
"main": "src/index.js", "main": "src/index.js",
"scripts": { "scripts": {
"dev": "node --watch src/index.js", "dev": "node --watch src/index.js",
"build": "node -c src/index.js && node -c src/routes/auth.routes.js && node -c src/routes/bookmarks.routes.js && node -c src/routes/folders.routes.js && node -c src/routes/importExport.routes.js && node -c src/routes/sync.routes.js", "build": "node -c src/index.js && node -c src/routes/auth.routes.js && node -c src/routes/bookmarks.routes.js && node -c src/routes/folders.routes.js && node -c src/routes/importExport.routes.js && node -c src/routes/sync.routes.js",
"test": "node --test", "test": "node --test",
"lint": "eslint .", "lint": "eslint .",
"db:migrate": "node src/migrate.js", "db:migrate": "node src/migrate.js",
"db:reset": "node src/resetDb.js" "db:reset": "node src/resetDb.js"
}, },
"dependencies": { "dependencies": {
"@browser-bookmark/shared": "0.1.0", "@browser-bookmark/shared": "file:../../packages/shared",
"@fastify/cors": "^11.2.0", "@fastify/cors": "^11.2.0",
"@fastify/jwt": "^10.0.0", "@fastify/jwt": "^10.0.0",
"@fastify/multipart": "^9.3.0", "@fastify/multipart": "^9.3.0",
"bcryptjs": "^3.0.3", "bcryptjs": "^3.0.3",
"cheerio": "^1.1.2", "cheerio": "^1.1.2",
"dotenv": "^16.4.7", "dotenv": "^16.4.7",
"fastify": "^5.2.1", "fastify": "^5.2.1",
"jsonwebtoken": "^9.0.3", "jsonwebtoken": "^9.0.3",
"pg": "^8.13.1" "pg": "^8.13.1"
}, },
"devDependencies": { "devDependencies": {
"eslint": "^9.17.0" "eslint": "^9.17.0"
}, },
"engines": { "engines": {
"node": ">=22" "node": ">=22"
} }
} }

View File

@@ -1,6 +1,6 @@
import test from "node:test"; import test from "node:test";
import assert from "node:assert/strict"; import assert from "node:assert/strict";
test("placeholder", () => { test("placeholder", () => {
assert.equal(1 + 1, 2); assert.equal(1 + 1, 2);
}); });

View File

@@ -1,44 +1,44 @@
import fs from "node:fs"; import fs from "node:fs";
import path from "node:path"; import path from "node:path";
import dotenv from "dotenv"; import dotenv from "dotenv";
function loadEnv() { function loadEnv() {
// When running via npm workspaces, cwd is often apps/server. // When running via npm workspaces, cwd is often apps/server.
// Support both apps/server/.env and repo-root/.env. // Support both apps/server/.env and repo-root/.env.
const candidates = [ const candidates = [
path.resolve(process.cwd(), ".env"), path.resolve(process.cwd(), ".env"),
path.resolve(process.cwd(), "..", "..", ".env") path.resolve(process.cwd(), "..", "..", ".env")
]; ];
for (const envPath of candidates) { for (const envPath of candidates) {
if (fs.existsSync(envPath)) { if (fs.existsSync(envPath)) {
dotenv.config({ path: envPath }); dotenv.config({ path: envPath });
return; return;
} }
} }
} }
loadEnv(); loadEnv();
export function getConfig() { export function getConfig() {
const serverPort = Number(process.env.SERVER_PORT || 3001); const serverPort = Number(process.env.SERVER_PORT || 3001);
const adminEmail = String(process.env.ADMIN_EMAIL || "").trim().toLowerCase(); const adminEmail = String(process.env.ADMIN_EMAIL || "").trim().toLowerCase();
const corsOriginsRaw = String(process.env.CORS_ORIGINS || "").trim(); const corsOriginsRaw = String(process.env.CORS_ORIGINS || "").trim();
const corsOrigins = corsOriginsRaw const corsOrigins = corsOriginsRaw
? corsOriginsRaw.split(",").map((item) => item.trim()).filter(Boolean) ? corsOriginsRaw.split(",").map((item) => item.trim()).filter(Boolean)
: true; : true;
return { return {
serverPort, serverPort,
adminEmail, adminEmail,
corsOrigins, corsOrigins,
database: { database: {
host: process.env.DATABASE_HOST || "127.0.0.1", host: process.env.DATABASE_HOST || "127.0.0.1",
port: Number(process.env.DATABASE_PORT || 5432), port: Number(process.env.DATABASE_PORT || 5432),
database: process.env.DATABASE_NAME || "postgres", database: process.env.DATABASE_NAME || "postgres",
user: process.env.DATABASE_USER || "postgres", user: process.env.DATABASE_USER || "postgres",
password: process.env.DATABASE_PASSWORD || "", password: process.env.DATABASE_PASSWORD || "",
ssl: String(process.env.DATABASE_SSL || "false").toLowerCase() === "true" ssl: String(process.env.DATABASE_SSL || "false").toLowerCase() === "true"
} }
}; };
} }

View File

@@ -1,16 +1,16 @@
import pg from "pg"; import pg from "pg";
import { getConfig } from "./config.js"; import { getConfig } from "./config.js";
const { Pool } = pg; const { Pool } = pg;
export function createPool() { export function createPool() {
const { database } = getConfig(); const { database } = getConfig();
return new Pool({ return new Pool({
host: database.host, host: database.host,
port: database.port, port: database.port,
database: database.database, database: database.database,
user: database.user, user: database.user,
password: database.password, password: database.password,
ssl: database.ssl ? { rejectUnauthorized: false } : false ssl: database.ssl ? { rejectUnauthorized: false } : false
}); });
} }

View File

@@ -1,87 +1,87 @@
import Fastify from "fastify"; import Fastify from "fastify";
import cors from "@fastify/cors"; import cors from "@fastify/cors";
import multipart from "@fastify/multipart"; import multipart from "@fastify/multipart";
import jwt from "@fastify/jwt"; import jwt from "@fastify/jwt";
import { getConfig } from "./config.js"; import { getConfig } from "./config.js";
import { createPool } from "./db.js"; import { createPool } from "./db.js";
import { authRoutes } from "./routes/auth.routes.js"; import { authRoutes } from "./routes/auth.routes.js";
import { adminRoutes } from "./routes/admin.routes.js"; import { adminRoutes } from "./routes/admin.routes.js";
import { foldersRoutes } from "./routes/folders.routes.js"; import { foldersRoutes } from "./routes/folders.routes.js";
import { bookmarksRoutes } from "./routes/bookmarks.routes.js"; import { bookmarksRoutes } from "./routes/bookmarks.routes.js";
import { importExportRoutes } from "./routes/importExport.routes.js"; import { importExportRoutes } from "./routes/importExport.routes.js";
import { syncRoutes } from "./routes/sync.routes.js"; import { syncRoutes } from "./routes/sync.routes.js";
const app = Fastify({ logger: true }); const app = Fastify({ logger: true });
// Plugins // Plugins
const config = getConfig(); const config = getConfig();
await app.register(cors, { await app.register(cors, {
origin: config.corsOrigins, origin: config.corsOrigins,
credentials: true, credentials: true,
methods: ["GET", "POST", "PATCH", "DELETE", "OPTIONS"], methods: ["GET", "POST", "PATCH", "DELETE", "OPTIONS"],
allowedHeaders: ["Content-Type", "Authorization", "Accept"] allowedHeaders: ["Content-Type", "Authorization", "Accept"]
}); });
await app.register(multipart); await app.register(multipart);
const jwtSecret = process.env.AUTH_JWT_SECRET; const jwtSecret = process.env.AUTH_JWT_SECRET;
if (!jwtSecret) { if (!jwtSecret) {
throw new Error("AUTH_JWT_SECRET is required"); throw new Error("AUTH_JWT_SECRET is required");
} }
await app.register(jwt, { secret: jwtSecret }); await app.register(jwt, { secret: jwtSecret });
const pool = createPool(); const pool = createPool();
app.decorate("pg", pool); app.decorate("pg", pool);
// Detect optional DB features (for backwards compatibility when migrations haven't run yet). // Detect optional DB features (for backwards compatibility when migrations haven't run yet).
async function hasColumn(tableName, columnName) { async function hasColumn(tableName, columnName) {
try { try {
const r = await app.pg.query( const r = await app.pg.query(
"select 1 from information_schema.columns where table_schema=current_schema() and table_name=$1 and column_name=$2 limit 1", "select 1 from information_schema.columns where table_schema=current_schema() and table_name=$1 and column_name=$2 limit 1",
[tableName, columnName] [tableName, columnName]
); );
return r.rowCount > 0; return r.rowCount > 0;
} catch { } catch {
return false; return false;
} }
} }
const folderSortOrderSupported = await hasColumn("bookmark_folders", "sort_order"); const folderSortOrderSupported = await hasColumn("bookmark_folders", "sort_order");
const bookmarkSortOrderSupported = await hasColumn("bookmarks", "sort_order"); const bookmarkSortOrderSupported = await hasColumn("bookmarks", "sort_order");
app.decorate("features", { app.decorate("features", {
folderSortOrder: folderSortOrderSupported, folderSortOrder: folderSortOrderSupported,
bookmarkSortOrder: bookmarkSortOrderSupported bookmarkSortOrder: bookmarkSortOrderSupported
}); });
app.decorate("authenticate", async (req, reply) => { app.decorate("authenticate", async (req, reply) => {
try { try {
await req.jwtVerify(); await req.jwtVerify();
} catch (err) { } catch (err) {
reply.code(401); reply.code(401);
throw err; throw err;
} }
}); });
app.setErrorHandler((err, _req, reply) => { app.setErrorHandler((err, _req, reply) => {
const statusCode = err.statusCode || 500; const statusCode = err.statusCode || 500;
reply.code(statusCode).send({ message: err.message || "server error" }); reply.code(statusCode).send({ message: err.message || "server error" });
}); });
app.get("/health", async () => ({ ok: true })); app.get("/health", async () => ({ ok: true }));
// Routes // Routes
app.decorate("config", config); app.decorate("config", config);
await authRoutes(app); await authRoutes(app);
await adminRoutes(app); await adminRoutes(app);
await foldersRoutes(app); await foldersRoutes(app);
await bookmarksRoutes(app); await bookmarksRoutes(app);
await importExportRoutes(app); await importExportRoutes(app);
await syncRoutes(app); await syncRoutes(app);
app.addHook("onClose", async (instance) => { app.addHook("onClose", async (instance) => {
await instance.pg.end(); await instance.pg.end();
}); });
const { serverPort } = config; const { serverPort } = config;
await app.listen({ port: serverPort, host: "0.0.0.0" }); await app.listen({ port: serverPort, host: "0.0.0.0" });

View File

@@ -1,29 +1,29 @@
import { httpError } from "./httpErrors.js"; import { httpError } from "./httpErrors.js";
function normalizeEmail(email) { function normalizeEmail(email) {
return String(email || "").trim().toLowerCase(); return String(email || "").trim().toLowerCase();
} }
export async function requireAdmin(app, req) { export async function requireAdmin(app, req) {
await app.authenticate(req); await app.authenticate(req);
const userId = req.user?.sub; const userId = req.user?.sub;
if (!userId) throw httpError(401, "unauthorized"); if (!userId) throw httpError(401, "unauthorized");
const res = await app.pg.query( const res = await app.pg.query(
"select id, email, role, created_at, updated_at from users where id=$1", "select id, email, role, created_at, updated_at from users where id=$1",
[userId] [userId]
); );
const row = res.rows[0]; const row = res.rows[0];
if (!row) throw httpError(401, "unauthorized"); if (!row) throw httpError(401, "unauthorized");
const adminEmail = normalizeEmail(app.config?.adminEmail); const adminEmail = normalizeEmail(app.config?.adminEmail);
const isAdmin = Boolean(adminEmail) && normalizeEmail(row.email) === adminEmail; const isAdmin = Boolean(adminEmail) && normalizeEmail(row.email) === adminEmail;
if (!isAdmin) throw httpError(403, "admin only"); if (!isAdmin) throw httpError(403, "admin only");
req.adminUser = row; req.adminUser = row;
} }
export function isAdminEmail(app, email) { export function isAdminEmail(app, email) {
const adminEmail = normalizeEmail(app.config?.adminEmail); const adminEmail = normalizeEmail(app.config?.adminEmail);
return Boolean(adminEmail) && normalizeEmail(email) === adminEmail; return Boolean(adminEmail) && normalizeEmail(email) === adminEmail;
} }

View File

@@ -1,10 +1,10 @@
import bcrypt from "bcryptjs"; import bcrypt from "bcryptjs";
export async function hashPassword(password) { export async function hashPassword(password) {
const saltRounds = 10; const saltRounds = 10;
return bcrypt.hash(password, saltRounds); return bcrypt.hash(password, saltRounds);
} }
export async function verifyPassword(password, passwordHash) { export async function verifyPassword(password, passwordHash) {
return bcrypt.compare(password, passwordHash); return bcrypt.compare(password, passwordHash);
} }

View File

@@ -1,125 +1,125 @@
import * as cheerio from "cheerio"; import * as cheerio from "cheerio";
export function parseNetscapeBookmarkHtmlNode(html) { export function parseNetscapeBookmarkHtmlNode(html) {
const $ = cheerio.load(html, { decodeEntities: false }); const $ = cheerio.load(html, { decodeEntities: false });
const rootDl = $("dl").first(); const rootDl = $("dl").first();
if (!rootDl.length) return { folders: [], bookmarks: [] }; if (!rootDl.length) return { folders: [], bookmarks: [] };
const folders = []; const folders = [];
const bookmarks = []; const bookmarks = [];
function normText(s) { function normText(s) {
return String(s || "").replace(/\s+/g, " ").trim(); return String(s || "").replace(/\s+/g, " ").trim();
} }
function collectLevelDt(node) { function collectLevelDt(node) {
const out = []; const out = [];
const children = $(node).contents().toArray(); const children = $(node).contents().toArray();
for (const child of children) { for (const child of children) {
if (!child || child.type !== "tag") continue; if (!child || child.type !== "tag") continue;
const tag = child.tagName?.toLowerCase(); const tag = child.tagName?.toLowerCase();
if (tag === "dt") { if (tag === "dt") {
out.push(child); out.push(child);
continue; continue;
} }
if (tag === "dl") { if (tag === "dl") {
// nested list belongs to the previous <DT> // nested list belongs to the previous <DT>
continue; continue;
} }
out.push(...collectLevelDt(child)); out.push(...collectLevelDt(child));
} }
return out; return out;
} }
function findNextDlForDt(dtNode, stopDlNode) { function findNextDlForDt(dtNode, stopDlNode) {
let cur = dtNode; let cur = dtNode;
while (cur && cur !== stopDlNode) { while (cur && cur !== stopDlNode) {
let next = cur.nextSibling; let next = cur.nextSibling;
while (next && next.type !== "tag") next = next.nextSibling; while (next && next.type !== "tag") next = next.nextSibling;
if (next && next.type === "tag" && next.tagName?.toLowerCase() === "dl") return $(next); if (next && next.type === "tag" && next.tagName?.toLowerCase() === "dl") return $(next);
cur = cur.parent; cur = cur.parent;
} }
return null; return null;
} }
function walkDl($dl, parentTempId) { function walkDl($dl, parentTempId) {
// Netscape format: <DL><p> contains repeating <DT> items and nested <DL>. // Netscape format: <DL><p> contains repeating <DT> items and nested <DL>.
// When parsed, <DT> may be wrapped (e.g. inside <p>), so we must be robust. // When parsed, <DT> may be wrapped (e.g. inside <p>), so we must be robust.
const dts = collectLevelDt($dl[0]); const dts = collectLevelDt($dl[0]);
for (const node of dts) { for (const node of dts) {
const $dt = $(node); const $dt = $(node);
const $h3 = $dt.children("h3").first().length ? $dt.children("h3").first() : $dt.find("h3").first(); const $h3 = $dt.children("h3").first().length ? $dt.children("h3").first() : $dt.find("h3").first();
const $a = $dt.children("a").first().length ? $dt.children("a").first() : $dt.find("a").first(); const $a = $dt.children("a").first().length ? $dt.children("a").first() : $dt.find("a").first();
const $nestedDl = $dt.children("dl").first(); const $nestedDl = $dt.children("dl").first();
const $nextDl = $nestedDl.length ? $nestedDl : findNextDlForDt(node, $dl[0]); const $nextDl = $nestedDl.length ? $nestedDl : findNextDlForDt(node, $dl[0]);
if ($h3.length) { if ($h3.length) {
const tempId = `${folders.length + 1}`; const tempId = `${folders.length + 1}`;
const name = normText($h3.text() || ""); const name = normText($h3.text() || "");
folders.push({ tempId, parentTempId: parentTempId ?? null, name }); folders.push({ tempId, parentTempId: parentTempId ?? null, name });
if ($nextDl?.length) walkDl($nextDl, tempId); if ($nextDl?.length) walkDl($nextDl, tempId);
} else if ($a.length) { } else if ($a.length) {
const title = normText($a.text() || ""); const title = normText($a.text() || "");
const url = $a.attr("href") || ""; const url = $a.attr("href") || "";
bookmarks.push({ parentTempId: parentTempId ?? null, title, url }); bookmarks.push({ parentTempId: parentTempId ?? null, title, url });
} }
} }
} }
walkDl(rootDl, null); walkDl(rootDl, null);
return { folders, bookmarks }; return { folders, bookmarks };
} }
export function buildNetscapeBookmarkHtml({ folders, bookmarks }) { export function buildNetscapeBookmarkHtml({ folders, bookmarks }) {
// folders: [{id, parentId, name}] // folders: [{id, parentId, name}]
// bookmarks: [{folderId, title, url}] // bookmarks: [{folderId, title, url}]
const folderChildren = new Map(); const folderChildren = new Map();
const bookmarkChildren = new Map(); const bookmarkChildren = new Map();
for (const f of folders) { for (const f of folders) {
const key = f.parentId ?? "root"; const key = f.parentId ?? "root";
if (!folderChildren.has(key)) folderChildren.set(key, []); if (!folderChildren.has(key)) folderChildren.set(key, []);
folderChildren.get(key).push(f); folderChildren.get(key).push(f);
} }
for (const b of bookmarks) { for (const b of bookmarks) {
const key = b.folderId ?? "root"; const key = b.folderId ?? "root";
if (!bookmarkChildren.has(key)) bookmarkChildren.set(key, []); if (!bookmarkChildren.has(key)) bookmarkChildren.set(key, []);
bookmarkChildren.get(key).push(b); bookmarkChildren.get(key).push(b);
} }
function esc(s) { function esc(s) {
return String(s) return String(s)
.replaceAll("&", "&amp;") .replaceAll("&", "&amp;")
.replaceAll("<", "&lt;") .replaceAll("<", "&lt;")
.replaceAll(">", "&gt;") .replaceAll(">", "&gt;")
.replaceAll('"', "&quot;"); .replaceAll('"', "&quot;");
} }
function renderFolder(parentId) { function renderFolder(parentId) {
const key = parentId ?? "root"; const key = parentId ?? "root";
const subFolders = (folderChildren.get(key) || []).slice().sort((a, b) => a.name.localeCompare(b.name)); const subFolders = (folderChildren.get(key) || []).slice().sort((a, b) => a.name.localeCompare(b.name));
const subBookmarks = (bookmarkChildren.get(key) || []).slice().sort((a, b) => a.title.localeCompare(b.title)); const subBookmarks = (bookmarkChildren.get(key) || []).slice().sort((a, b) => a.title.localeCompare(b.title));
let out = "<DL><p>\n"; let out = "<DL><p>\n";
for (const f of subFolders) { for (const f of subFolders) {
out += ` <DT><H3>${esc(f.name)}</H3>\n`; out += ` <DT><H3>${esc(f.name)}</H3>\n`;
out += renderFolder(f.id) out += renderFolder(f.id)
.split("\n") .split("\n")
.map((line) => (line ? ` ${line}` : line)) .map((line) => (line ? ` ${line}` : line))
.join("\n"); .join("\n");
out += "\n"; out += "\n";
} }
for (const b of subBookmarks) { for (const b of subBookmarks) {
out += ` <DT><A HREF=\"${esc(b.url)}\">${esc(b.title)}</A>\n`; out += ` <DT><A HREF=\"${esc(b.url)}\">${esc(b.title)}</A>\n`;
} }
out += "</DL><p>"; out += "</DL><p>";
return out; return out;
} }
const header = `<!DOCTYPE NETSCAPE-Bookmark-file-1>\n<!-- This is an automatically generated file. -->\n<META HTTP-EQUIV=\"Content-Type\" CONTENT=\"text/html; charset=UTF-8\">\n<TITLE>Bookmarks</TITLE>\n<H1>Bookmarks</H1>\n`; const header = `<!DOCTYPE NETSCAPE-Bookmark-file-1>\n<!-- This is an automatically generated file. -->\n<META HTTP-EQUIV=\"Content-Type\" CONTENT=\"text/html; charset=UTF-8\">\n<TITLE>Bookmarks</TITLE>\n<H1>Bookmarks</H1>\n`;
const body = renderFolder(null); const body = renderFolder(null);
return header + body + "\n"; return header + body + "\n";
} }

View File

@@ -1,5 +1,5 @@
export function httpError(statusCode, message) { export function httpError(statusCode, message) {
const err = new Error(message); const err = new Error(message);
err.statusCode = statusCode; err.statusCode = statusCode;
return err; return err;
} }

View File

@@ -1,39 +1,39 @@
export function userRowToDto(row) { export function userRowToDto(row) {
return { return {
id: row.id, id: row.id,
email: row.email, email: row.email,
role: row.role, role: row.role,
createdAt: row.created_at, createdAt: row.created_at,
updatedAt: row.updated_at updatedAt: row.updated_at
}; };
} }
export function folderRowToDto(row) { export function folderRowToDto(row) {
return { return {
id: row.id, id: row.id,
userId: row.user_id, userId: row.user_id,
parentId: row.parent_id, parentId: row.parent_id,
name: row.name, name: row.name,
visibility: row.visibility, visibility: row.visibility,
sortOrder: row.sort_order ?? 0, sortOrder: row.sort_order ?? 0,
createdAt: row.created_at, createdAt: row.created_at,
updatedAt: row.updated_at updatedAt: row.updated_at
}; };
} }
export function bookmarkRowToDto(row) { export function bookmarkRowToDto(row) {
return { return {
id: row.id, id: row.id,
userId: row.user_id, userId: row.user_id,
folderId: row.folder_id, folderId: row.folder_id,
sortOrder: row.sort_order ?? 0, sortOrder: row.sort_order ?? 0,
title: row.title, title: row.title,
url: row.url, url: row.url,
urlNormalized: row.url_normalized, urlNormalized: row.url_normalized,
urlHash: row.url_hash, urlHash: row.url_hash,
visibility: row.visibility, visibility: row.visibility,
source: row.source, source: row.source,
updatedAt: row.updated_at, updatedAt: row.updated_at,
deletedAt: row.deleted_at deletedAt: row.deleted_at
}; };
} }

View File

@@ -1,61 +1,61 @@
import { readFile, readdir } from "node:fs/promises"; import { readFile, readdir } from "node:fs/promises";
import path from "node:path"; import path from "node:path";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
import { createPool } from "./db.js"; import { createPool } from "./db.js";
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename); const __dirname = path.dirname(__filename);
async function ensureMigrationsTable(pool) { async function ensureMigrationsTable(pool) {
await pool.query(` await pool.query(`
create table if not exists schema_migrations ( create table if not exists schema_migrations (
id text primary key, id text primary key,
applied_at timestamptz not null default now() applied_at timestamptz not null default now()
); );
`); `);
} }
async function getApplied(pool) { async function getApplied(pool) {
const res = await pool.query("select id from schema_migrations order by id"); const res = await pool.query("select id from schema_migrations order by id");
return new Set(res.rows.map((r) => r.id)); return new Set(res.rows.map((r) => r.id));
} }
async function applyMigration(pool, id, sql) { async function applyMigration(pool, id, sql) {
await pool.query("begin"); await pool.query("begin");
try { try {
await pool.query(sql); await pool.query(sql);
await pool.query("insert into schema_migrations (id) values ($1)", [id]); await pool.query("insert into schema_migrations (id) values ($1)", [id]);
await pool.query("commit"); await pool.query("commit");
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log(`[migrate] applied ${id}`); console.log(`[migrate] applied ${id}`);
} catch (err) { } catch (err) {
await pool.query("rollback"); await pool.query("rollback");
throw err; throw err;
} }
} }
async function main() { async function main() {
const pool = createPool(); const pool = createPool();
try { try {
await ensureMigrationsTable(pool); await ensureMigrationsTable(pool);
const applied = await getApplied(pool); const applied = await getApplied(pool);
const migrationsDir = path.resolve(__dirname, "..", "migrations"); const migrationsDir = path.resolve(__dirname, "..", "migrations");
const files = (await readdir(migrationsDir)) const files = (await readdir(migrationsDir))
.filter((f) => f.endsWith(".sql")) .filter((f) => f.endsWith(".sql"))
.sort(); .sort();
for (const file of files) { for (const file of files) {
if (applied.has(file)) continue; if (applied.has(file)) continue;
const sql = await readFile(path.join(migrationsDir, file), "utf8"); const sql = await readFile(path.join(migrationsDir, file), "utf8");
await applyMigration(pool, file, sql); await applyMigration(pool, file, sql);
} }
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log("[migrate] done"); console.log("[migrate] done");
} finally { } finally {
await pool.end(); await pool.end();
} }
} }
main(); main();

View File

@@ -1,26 +1,26 @@
import { createPool } from "./db.js"; import { createPool } from "./db.js";
async function main() { async function main() {
const pool = createPool(); const pool = createPool();
try { try {
// Destructive: development convenience only. // Destructive: development convenience only.
await pool.query("begin"); await pool.query("begin");
try { try {
await pool.query("drop table if exists bookmarks cascade"); await pool.query("drop table if exists bookmarks cascade");
await pool.query("drop table if exists bookmark_folders cascade"); await pool.query("drop table if exists bookmark_folders cascade");
await pool.query("drop table if exists users cascade"); await pool.query("drop table if exists users cascade");
await pool.query("drop table if exists schema_migrations cascade"); await pool.query("drop table if exists schema_migrations cascade");
await pool.query("commit"); await pool.query("commit");
} catch (e) { } catch (e) {
await pool.query("rollback"); await pool.query("rollback");
throw e; throw e;
} }
} finally { } finally {
await pool.end(); await pool.end();
} }
// Re-apply migrations. // Re-apply migrations.
await import("./migrate.js"); await import("./migrate.js");
} }
main(); main();

View File

@@ -1,147 +1,147 @@
import { computeUrlHash, normalizeUrl } from "@browser-bookmark/shared"; import { computeUrlHash, normalizeUrl } from "@browser-bookmark/shared";
import { httpError } from "../lib/httpErrors.js"; import { httpError } from "../lib/httpErrors.js";
import { requireAdmin, isAdminEmail } from "../lib/admin.js"; import { requireAdmin, isAdminEmail } from "../lib/admin.js";
import { bookmarkRowToDto, folderRowToDto, userRowToDto } from "../lib/rows.js"; import { bookmarkRowToDto, folderRowToDto, userRowToDto } from "../lib/rows.js";
function toUserDtoWithAdminOverride(app, row) { function toUserDtoWithAdminOverride(app, row) {
const dto = userRowToDto(row); const dto = userRowToDto(row);
if (isAdminEmail(app, dto.email)) dto.role = "admin"; if (isAdminEmail(app, dto.email)) dto.role = "admin";
return dto; return dto;
} }
export async function adminRoutes(app) { export async function adminRoutes(app) {
app.get( app.get(
"/admin/users", "/admin/users",
{ preHandler: [async (req) => requireAdmin(app, req)] }, { preHandler: [async (req) => requireAdmin(app, req)] },
async () => { async () => {
const res = await app.pg.query( const res = await app.pg.query(
"select id, email, role, created_at, updated_at from users order by created_at desc limit 500" "select id, email, role, created_at, updated_at from users order by created_at desc limit 500"
); );
return res.rows.map((r) => toUserDtoWithAdminOverride(app, r)); return res.rows.map((r) => toUserDtoWithAdminOverride(app, r));
} }
); );
app.get( app.get(
"/admin/users/:id/folders", "/admin/users/:id/folders",
{ preHandler: [async (req) => requireAdmin(app, req)] }, { preHandler: [async (req) => requireAdmin(app, req)] },
async (req) => { async (req) => {
const userId = req.params?.id; const userId = req.params?.id;
if (!userId) throw httpError(400, "user id required"); if (!userId) throw httpError(400, "user id required");
const orderBy = app.features?.folderSortOrder const orderBy = app.features?.folderSortOrder
? "parent_id nulls first, sort_order asc, name asc" ? "parent_id nulls first, sort_order asc, name asc"
: "parent_id nulls first, name asc"; : "parent_id nulls first, name asc";
const res = await app.pg.query( const res = await app.pg.query(
`select * from bookmark_folders where user_id=$1 order by ${orderBy} limit 1000`, `select * from bookmark_folders where user_id=$1 order by ${orderBy} limit 1000`,
[userId] [userId]
); );
return res.rows.map(folderRowToDto); return res.rows.map(folderRowToDto);
} }
); );
app.get( app.get(
"/admin/users/:id/bookmarks", "/admin/users/:id/bookmarks",
{ preHandler: [async (req) => requireAdmin(app, req)] }, { preHandler: [async (req) => requireAdmin(app, req)] },
async (req) => { async (req) => {
const userId = req.params?.id; const userId = req.params?.id;
if (!userId) throw httpError(400, "user id required"); if (!userId) throw httpError(400, "user id required");
const q = (req.query?.q || "").trim(); const q = (req.query?.q || "").trim();
const params = [userId]; const params = [userId];
let where = "where user_id=$1 and deleted_at is null"; let where = "where user_id=$1 and deleted_at is null";
if (q) { if (q) {
params.push(`%${q}%`); params.push(`%${q}%`);
where += ` and (title ilike $${params.length} or url ilike $${params.length})`; where += ` and (title ilike $${params.length} or url ilike $${params.length})`;
} }
const orderBy = app.features?.bookmarkSortOrder const orderBy = app.features?.bookmarkSortOrder
? "folder_id nulls first, sort_order asc, updated_at desc" ? "folder_id nulls first, sort_order asc, updated_at desc"
: "updated_at desc"; : "updated_at desc";
const res = await app.pg.query( const res = await app.pg.query(
`select * from bookmarks ${where} order by ${orderBy} limit 500`, `select * from bookmarks ${where} order by ${orderBy} limit 500`,
params params
); );
return res.rows.map(bookmarkRowToDto); return res.rows.map(bookmarkRowToDto);
} }
); );
app.delete( app.delete(
"/admin/users/:userId/bookmarks/:bookmarkId", "/admin/users/:userId/bookmarks/:bookmarkId",
{ preHandler: [async (req) => requireAdmin(app, req)] }, { preHandler: [async (req) => requireAdmin(app, req)] },
async (req) => { async (req) => {
const userId = req.params?.userId; const userId = req.params?.userId;
const bookmarkId = req.params?.bookmarkId; const bookmarkId = req.params?.bookmarkId;
if (!userId || !bookmarkId) throw httpError(400, "userId and bookmarkId required"); if (!userId || !bookmarkId) throw httpError(400, "userId and bookmarkId required");
const res = await app.pg.query( const res = await app.pg.query(
"update bookmarks set deleted_at=now(), updated_at=now() where id=$1 and user_id=$2 and deleted_at is null returning *", "update bookmarks set deleted_at=now(), updated_at=now() where id=$1 and user_id=$2 and deleted_at is null returning *",
[bookmarkId, userId] [bookmarkId, userId]
); );
if (!res.rows[0]) throw httpError(404, "bookmark not found"); if (!res.rows[0]) throw httpError(404, "bookmark not found");
return bookmarkRowToDto(res.rows[0]); return bookmarkRowToDto(res.rows[0]);
} }
); );
app.delete( app.delete(
"/admin/users/:userId/folders/:folderId", "/admin/users/:userId/folders/:folderId",
{ preHandler: [async (req) => requireAdmin(app, req)] }, { preHandler: [async (req) => requireAdmin(app, req)] },
async (req) => { async (req) => {
const userId = req.params?.userId; const userId = req.params?.userId;
const folderId = req.params?.folderId; const folderId = req.params?.folderId;
if (!userId || !folderId) throw httpError(400, "userId and folderId required"); if (!userId || !folderId) throw httpError(400, "userId and folderId required");
const res = await app.pg.query( const res = await app.pg.query(
"delete from bookmark_folders where id=$1 and user_id=$2 returning *", "delete from bookmark_folders where id=$1 and user_id=$2 returning *",
[folderId, userId] [folderId, userId]
); );
if (!res.rows[0]) throw httpError(404, "folder not found"); if (!res.rows[0]) throw httpError(404, "folder not found");
return folderRowToDto(res.rows[0]); return folderRowToDto(res.rows[0]);
} }
); );
app.post( app.post(
"/admin/users/:userId/bookmarks/:bookmarkId/copy-to-me", "/admin/users/:userId/bookmarks/:bookmarkId/copy-to-me",
{ preHandler: [async (req) => requireAdmin(app, req)] }, { preHandler: [async (req) => requireAdmin(app, req)] },
async (req) => { async (req) => {
const sourceUserId = req.params?.userId; const sourceUserId = req.params?.userId;
const bookmarkId = req.params?.bookmarkId; const bookmarkId = req.params?.bookmarkId;
const adminUserId = req.adminUser?.id; const adminUserId = req.adminUser?.id;
if (!sourceUserId || !bookmarkId) throw httpError(400, "userId and bookmarkId required"); if (!sourceUserId || !bookmarkId) throw httpError(400, "userId and bookmarkId required");
if (!adminUserId) throw httpError(401, "unauthorized"); if (!adminUserId) throw httpError(401, "unauthorized");
const srcRes = await app.pg.query( const srcRes = await app.pg.query(
"select * from bookmarks where id=$1 and user_id=$2 and deleted_at is null", "select * from bookmarks where id=$1 and user_id=$2 and deleted_at is null",
[bookmarkId, sourceUserId] [bookmarkId, sourceUserId]
); );
const src = srcRes.rows[0]; const src = srcRes.rows[0];
if (!src) throw httpError(404, "bookmark not found"); if (!src) throw httpError(404, "bookmark not found");
const urlNormalized = normalizeUrl(src.url); const urlNormalized = normalizeUrl(src.url);
const urlHash = computeUrlHash(urlNormalized); const urlHash = computeUrlHash(urlNormalized);
const existing = await app.pg.query( const existing = await app.pg.query(
"select * from bookmarks where user_id=$1 and url_hash=$2 and deleted_at is null limit 1", "select * from bookmarks where user_id=$1 and url_hash=$2 and deleted_at is null limit 1",
[adminUserId, urlHash] [adminUserId, urlHash]
); );
if (existing.rows[0]) { if (existing.rows[0]) {
const merged = await app.pg.query( const merged = await app.pg.query(
`update bookmarks `update bookmarks
set title=$1, url=$2, url_normalized=$3, visibility='private', folder_id=null, source='manual', updated_at=now() set title=$1, url=$2, url_normalized=$3, visibility='private', folder_id=null, source='manual', updated_at=now()
where id=$4 where id=$4
returning *`, returning *`,
[src.title, src.url, urlNormalized, existing.rows[0].id] [src.title, src.url, urlNormalized, existing.rows[0].id]
); );
return bookmarkRowToDto(merged.rows[0]); return bookmarkRowToDto(merged.rows[0]);
} }
const res = await app.pg.query( const res = await app.pg.query(
`insert into bookmarks (user_id, folder_id, title, url, url_normalized, url_hash, visibility, source) `insert into bookmarks (user_id, folder_id, title, url, url_normalized, url_hash, visibility, source)
values ($1, null, $2, $3, $4, $5, 'private', 'manual') values ($1, null, $2, $3, $4, $5, 'private', 'manual')
returning *`, returning *`,
[adminUserId, src.title, src.url, urlNormalized, urlHash] [adminUserId, src.title, src.url, urlNormalized, urlHash]
); );
return bookmarkRowToDto(res.rows[0]); return bookmarkRowToDto(res.rows[0]);
} }
); );
} }

View File

@@ -1,74 +1,74 @@
import { hashPassword, verifyPassword } from "../lib/auth.js"; import { hashPassword, verifyPassword } from "../lib/auth.js";
import { httpError } from "../lib/httpErrors.js"; import { httpError } from "../lib/httpErrors.js";
import { userRowToDto } from "../lib/rows.js"; import { userRowToDto } from "../lib/rows.js";
function normalizeEmail(email) { function normalizeEmail(email) {
return String(email || "").trim().toLowerCase(); return String(email || "").trim().toLowerCase();
} }
function toUserDtoWithAdminOverride(app, row) { function toUserDtoWithAdminOverride(app, row) {
const dto = userRowToDto(row); const dto = userRowToDto(row);
const adminEmail = normalizeEmail(app.config?.adminEmail); const adminEmail = normalizeEmail(app.config?.adminEmail);
if (adminEmail && normalizeEmail(dto.email) === adminEmail) { if (adminEmail && normalizeEmail(dto.email) === adminEmail) {
dto.role = "admin"; dto.role = "admin";
} }
return dto; return dto;
} }
export async function authRoutes(app) { export async function authRoutes(app) {
app.post("/auth/register", async (req) => { app.post("/auth/register", async (req) => {
const { email, password } = req.body || {}; const { email, password } = req.body || {};
if (!email || !password) throw httpError(400, "email and password required"); if (!email || !password) throw httpError(400, "email and password required");
if (String(password).length < 8) throw httpError(400, "password too short"); if (String(password).length < 8) throw httpError(400, "password too short");
const passwordHash = await hashPassword(password); const passwordHash = await hashPassword(password);
try { try {
const res = await app.pg.query( const res = await app.pg.query(
"insert into users (email, password_hash) values ($1, $2) returning id, email, role, created_at, updated_at", "insert into users (email, password_hash) values ($1, $2) returning id, email, role, created_at, updated_at",
[email, passwordHash] [email, passwordHash]
); );
const user = toUserDtoWithAdminOverride(app, res.rows[0]); const user = toUserDtoWithAdminOverride(app, res.rows[0]);
const token = await app.jwt.sign({ sub: user.id, role: user.role }); const token = await app.jwt.sign({ sub: user.id, role: user.role });
return { token, user }; return { token, user };
} catch (err) { } catch (err) {
if (String(err?.code) === "23505") throw httpError(409, "email already exists"); if (String(err?.code) === "23505") throw httpError(409, "email already exists");
throw err; throw err;
} }
}); });
app.post("/auth/login", async (req) => { app.post("/auth/login", async (req) => {
const { email, password } = req.body || {}; const { email, password } = req.body || {};
if (!email || !password) throw httpError(400, "email and password required"); if (!email || !password) throw httpError(400, "email and password required");
const res = await app.pg.query( const res = await app.pg.query(
"select id, email, role, password_hash, created_at, updated_at from users where email=$1", "select id, email, role, password_hash, created_at, updated_at from users where email=$1",
[email] [email]
); );
const row = res.rows[0]; const row = res.rows[0];
if (!row) throw httpError(401, "invalid credentials"); if (!row) throw httpError(401, "invalid credentials");
const ok = await verifyPassword(password, row.password_hash); const ok = await verifyPassword(password, row.password_hash);
if (!ok) throw httpError(401, "invalid credentials"); if (!ok) throw httpError(401, "invalid credentials");
const user = userRowToDto(row); const user = userRowToDto(row);
const userWithRole = toUserDtoWithAdminOverride(app, row); const userWithRole = toUserDtoWithAdminOverride(app, row);
const token = await app.jwt.sign({ sub: userWithRole.id, role: userWithRole.role }); const token = await app.jwt.sign({ sub: userWithRole.id, role: userWithRole.role });
return { token, user: userWithRole }; return { token, user: userWithRole };
}); });
app.get( app.get(
"/auth/me", "/auth/me",
{ preHandler: [app.authenticate] }, { preHandler: [app.authenticate] },
async (req) => { async (req) => {
const userId = req.user.sub; const userId = req.user.sub;
const res = await app.pg.query( const res = await app.pg.query(
"select id, email, role, created_at, updated_at from users where id=$1", "select id, email, role, created_at, updated_at from users where id=$1",
[userId] [userId]
); );
const row = res.rows[0]; const row = res.rows[0];
if (!row) throw httpError(404, "user not found"); if (!row) throw httpError(404, "user not found");
return toUserDtoWithAdminOverride(app, row); return toUserDtoWithAdminOverride(app, row);
} }
); );
} }

View File

@@ -1,305 +1,305 @@
import { computeUrlHash, normalizeUrl } from "@browser-bookmark/shared"; import { computeUrlHash, normalizeUrl } from "@browser-bookmark/shared";
import { httpError } from "../lib/httpErrors.js"; import { httpError } from "../lib/httpErrors.js";
import { bookmarkRowToDto } from "../lib/rows.js"; import { bookmarkRowToDto } from "../lib/rows.js";
export async function bookmarksRoutes(app) { export async function bookmarksRoutes(app) {
app.get("/bookmarks/public", async (req) => { app.get("/bookmarks/public", async (req) => {
const q = (req.query?.q || "").trim(); const q = (req.query?.q || "").trim();
const params = []; const params = [];
let where = "where visibility='public' and deleted_at is null"; let where = "where visibility='public' and deleted_at is null";
if (q) { if (q) {
params.push(`%${q}%`); params.push(`%${q}%`);
where += ` and (title ilike $${params.length} or url ilike $${params.length})`; where += ` and (title ilike $${params.length} or url ilike $${params.length})`;
} }
const res = await app.pg.query( const res = await app.pg.query(
`select * from bookmarks ${where} order by updated_at desc limit 200`, `select * from bookmarks ${where} order by updated_at desc limit 200`,
params params
); );
return res.rows.map(bookmarkRowToDto); return res.rows.map(bookmarkRowToDto);
}); });
app.get( app.get(
"/bookmarks", "/bookmarks",
{ preHandler: [app.authenticate] }, { preHandler: [app.authenticate] },
async (req) => { async (req) => {
const userId = req.user.sub; const userId = req.user.sub;
const q = (req.query?.q || "").trim(); const q = (req.query?.q || "").trim();
const params = [userId]; const params = [userId];
let where = "where user_id=$1 and deleted_at is null"; let where = "where user_id=$1 and deleted_at is null";
if (q) { if (q) {
params.push(`%${q}%`); params.push(`%${q}%`);
where += ` and (title ilike $${params.length} or url ilike $${params.length})`; where += ` and (title ilike $${params.length} or url ilike $${params.length})`;
} }
const orderBy = app.features?.bookmarkSortOrder const orderBy = app.features?.bookmarkSortOrder
? "folder_id nulls first, sort_order asc, updated_at desc" ? "folder_id nulls first, sort_order asc, updated_at desc"
: "updated_at desc"; : "updated_at desc";
const res = await app.pg.query( const res = await app.pg.query(
`select * from bookmarks ${where} order by ${orderBy} limit 500`, `select * from bookmarks ${where} order by ${orderBy} limit 500`,
params params
); );
return res.rows.map(bookmarkRowToDto); return res.rows.map(bookmarkRowToDto);
} }
); );
app.post( app.post(
"/bookmarks", "/bookmarks",
{ preHandler: [app.authenticate] }, { preHandler: [app.authenticate] },
async (req) => { async (req) => {
const userId = req.user.sub; const userId = req.user.sub;
const { folderId, title, url, visibility } = req.body || {}; const { folderId, title, url, visibility } = req.body || {};
if (!title) throw httpError(400, "title required"); if (!title) throw httpError(400, "title required");
if (!url) throw httpError(400, "url required"); if (!url) throw httpError(400, "url required");
if (!visibility) throw httpError(400, "visibility required"); if (!visibility) throw httpError(400, "visibility required");
const urlNormalized = normalizeUrl(url); const urlNormalized = normalizeUrl(url);
const urlHash = computeUrlHash(urlNormalized); const urlHash = computeUrlHash(urlNormalized);
const existing = await app.pg.query( const existing = await app.pg.query(
"select * from bookmarks where user_id=$1 and url_hash=$2 and deleted_at is null limit 1", "select * from bookmarks where user_id=$1 and url_hash=$2 and deleted_at is null limit 1",
[userId, urlHash] [userId, urlHash]
); );
if (existing.rows[0]) { if (existing.rows[0]) {
// auto-merge // auto-merge
const targetFolderId = folderId ?? null; const targetFolderId = folderId ?? null;
const merged = app.features?.bookmarkSortOrder const merged = app.features?.bookmarkSortOrder
? await app.pg.query( ? await app.pg.query(
`update bookmarks `update bookmarks
set title=$1, set title=$1,
url=$2, url=$2,
url_normalized=$3, url_normalized=$3,
visibility=$4, visibility=$4,
folder_id=$5, folder_id=$5,
sort_order = case sort_order = case
when folder_id is distinct from $5 then ( when folder_id is distinct from $5 then (
select coalesce(max(sort_order), -1) + 1 select coalesce(max(sort_order), -1) + 1
from bookmarks from bookmarks
where user_id=$7 and folder_id is not distinct from $5 and deleted_at is null where user_id=$7 and folder_id is not distinct from $5 and deleted_at is null
) )
else sort_order else sort_order
end, end,
source='manual', source='manual',
updated_at=now() updated_at=now()
where id=$6 where id=$6
returning *`, returning *`,
[title, url, urlNormalized, visibility, targetFolderId, existing.rows[0].id, userId] [title, url, urlNormalized, visibility, targetFolderId, existing.rows[0].id, userId]
) )
: await app.pg.query( : await app.pg.query(
`update bookmarks `update bookmarks
set title=$1, url=$2, url_normalized=$3, visibility=$4, folder_id=$5, source='manual', updated_at=now() set title=$1, url=$2, url_normalized=$3, visibility=$4, folder_id=$5, source='manual', updated_at=now()
where id=$6 where id=$6
returning *`, returning *`,
[title, url, urlNormalized, visibility, targetFolderId, existing.rows[0].id] [title, url, urlNormalized, visibility, targetFolderId, existing.rows[0].id]
); );
return bookmarkRowToDto(merged.rows[0]); return bookmarkRowToDto(merged.rows[0]);
} }
const targetFolderId = folderId ?? null; const targetFolderId = folderId ?? null;
const res = app.features?.bookmarkSortOrder const res = app.features?.bookmarkSortOrder
? await app.pg.query( ? await app.pg.query(
`insert into bookmarks (user_id, folder_id, sort_order, title, url, url_normalized, url_hash, visibility, source) `insert into bookmarks (user_id, folder_id, sort_order, title, url, url_normalized, url_hash, visibility, source)
values ( values (
$1, $1,
$2, $2,
(select coalesce(max(sort_order), -1) + 1 from bookmarks where user_id=$1 and folder_id is not distinct from $2 and deleted_at is null), (select coalesce(max(sort_order), -1) + 1 from bookmarks where user_id=$1 and folder_id is not distinct from $2 and deleted_at is null),
$3, $3,
$4, $4,
$5, $5,
$6, $6,
$7, $7,
'manual' 'manual'
) )
returning *`, returning *`,
[userId, targetFolderId, title, url, urlNormalized, urlHash, visibility] [userId, targetFolderId, title, url, urlNormalized, urlHash, visibility]
) )
: await app.pg.query( : await app.pg.query(
`insert into bookmarks (user_id, folder_id, title, url, url_normalized, url_hash, visibility, source) `insert into bookmarks (user_id, folder_id, title, url, url_normalized, url_hash, visibility, source)
values ($1, $2, $3, $4, $5, $6, $7, 'manual') values ($1, $2, $3, $4, $5, $6, $7, 'manual')
returning *`, returning *`,
[userId, targetFolderId, title, url, urlNormalized, urlHash, visibility] [userId, targetFolderId, title, url, urlNormalized, urlHash, visibility]
); );
return bookmarkRowToDto(res.rows[0]); return bookmarkRowToDto(res.rows[0]);
} }
); );
app.post( app.post(
"/bookmarks/reorder", "/bookmarks/reorder",
{ preHandler: [app.authenticate] }, { preHandler: [app.authenticate] },
async (req) => { async (req) => {
if (!app.features?.bookmarkSortOrder) { if (!app.features?.bookmarkSortOrder) {
throw httpError( throw httpError(
409, 409,
"bookmark sort order is not supported by current database schema. Please run server migrations (db:migrate)." "bookmark sort order is not supported by current database schema. Please run server migrations (db:migrate)."
); );
} }
const userId = req.user.sub; const userId = req.user.sub;
const { folderId, orderedIds } = req.body || {}; const { folderId, orderedIds } = req.body || {};
const folder = folderId ?? null; const folder = folderId ?? null;
if (!Array.isArray(orderedIds) || orderedIds.length === 0) { if (!Array.isArray(orderedIds) || orderedIds.length === 0) {
throw httpError(400, "orderedIds required"); throw httpError(400, "orderedIds required");
} }
const siblings = await app.pg.query( const siblings = await app.pg.query(
"select id from bookmarks where user_id=$1 and folder_id is not distinct from $2 and deleted_at is null", "select id from bookmarks where user_id=$1 and folder_id is not distinct from $2 and deleted_at is null",
[userId, folder] [userId, folder]
); );
const siblingIds = siblings.rows.map((r) => r.id); const siblingIds = siblings.rows.map((r) => r.id);
const want = new Set(orderedIds); const want = new Set(orderedIds);
if (want.size !== orderedIds.length) throw httpError(400, "orderedIds must be unique"); if (want.size !== orderedIds.length) throw httpError(400, "orderedIds must be unique");
if (siblingIds.length !== orderedIds.length) throw httpError(400, "orderedIds must include all bookmarks in the folder"); if (siblingIds.length !== orderedIds.length) throw httpError(400, "orderedIds must include all bookmarks in the folder");
for (const id of siblingIds) { for (const id of siblingIds) {
if (!want.has(id)) throw httpError(400, "orderedIds must include all bookmarks in the folder"); if (!want.has(id)) throw httpError(400, "orderedIds must include all bookmarks in the folder");
} }
await app.pg.query("begin"); await app.pg.query("begin");
try { try {
for (let i = 0; i < orderedIds.length; i++) { for (let i = 0; i < orderedIds.length; i++) {
await app.pg.query( await app.pg.query(
"update bookmarks set sort_order=$1, updated_at=now() where id=$2 and user_id=$3 and deleted_at is null", "update bookmarks set sort_order=$1, updated_at=now() where id=$2 and user_id=$3 and deleted_at is null",
[i, orderedIds[i], userId] [i, orderedIds[i], userId]
); );
} }
await app.pg.query("commit"); await app.pg.query("commit");
} catch (e) { } catch (e) {
await app.pg.query("rollback"); await app.pg.query("rollback");
throw e; throw e;
} }
return { ok: true }; return { ok: true };
} }
); );
app.patch( app.patch(
"/bookmarks/:id", "/bookmarks/:id",
{ preHandler: [app.authenticate] }, { preHandler: [app.authenticate] },
async (req) => { async (req) => {
const userId = req.user.sub; const userId = req.user.sub;
const id = req.params?.id; const id = req.params?.id;
const body = req.body || {}; const body = req.body || {};
const existingRes = await app.pg.query( const existingRes = await app.pg.query(
"select * from bookmarks where id=$1 and user_id=$2 and deleted_at is null", "select * from bookmarks where id=$1 and user_id=$2 and deleted_at is null",
[id, userId] [id, userId]
); );
const existing = existingRes.rows[0]; const existing = existingRes.rows[0];
if (!existing) throw httpError(404, "bookmark not found"); if (!existing) throw httpError(404, "bookmark not found");
const sets = []; const sets = [];
const params = []; const params = [];
let i = 1; let i = 1;
// url update implies url_normalized + url_hash update // url update implies url_normalized + url_hash update
let nextUrl = existing.url; let nextUrl = existing.url;
if (Object.prototype.hasOwnProperty.call(body, "url")) { if (Object.prototype.hasOwnProperty.call(body, "url")) {
nextUrl = String(body.url || "").trim(); nextUrl = String(body.url || "").trim();
if (!nextUrl) throw httpError(400, "url required"); if (!nextUrl) throw httpError(400, "url required");
} }
let urlNormalized = existing.url_normalized; let urlNormalized = existing.url_normalized;
let urlHash = existing.url_hash; let urlHash = existing.url_hash;
const urlChanged = nextUrl !== existing.url; const urlChanged = nextUrl !== existing.url;
if (urlChanged) { if (urlChanged) {
urlNormalized = normalizeUrl(nextUrl); urlNormalized = normalizeUrl(nextUrl);
urlHash = computeUrlHash(urlNormalized); urlHash = computeUrlHash(urlNormalized);
} }
if (Object.prototype.hasOwnProperty.call(body, "title")) { if (Object.prototype.hasOwnProperty.call(body, "title")) {
const title = String(body.title || "").trim(); const title = String(body.title || "").trim();
if (!title) throw httpError(400, "title required"); if (!title) throw httpError(400, "title required");
sets.push(`title=$${i++}`); sets.push(`title=$${i++}`);
params.push(title); params.push(title);
} }
if (Object.prototype.hasOwnProperty.call(body, "folderId")) { if (Object.prototype.hasOwnProperty.call(body, "folderId")) {
sets.push(`folder_id=$${i++}`); sets.push(`folder_id=$${i++}`);
params.push(body.folderId ?? null); params.push(body.folderId ?? null);
} }
if (Object.prototype.hasOwnProperty.call(body, "visibility")) { if (Object.prototype.hasOwnProperty.call(body, "visibility")) {
if (!body.visibility) throw httpError(400, "visibility required"); if (!body.visibility) throw httpError(400, "visibility required");
sets.push(`visibility=$${i++}`); sets.push(`visibility=$${i++}`);
params.push(body.visibility); params.push(body.visibility);
} }
if (Object.prototype.hasOwnProperty.call(body, "sortOrder")) { if (Object.prototype.hasOwnProperty.call(body, "sortOrder")) {
if (!app.features?.bookmarkSortOrder) { if (!app.features?.bookmarkSortOrder) {
throw httpError( throw httpError(
409, 409,
"sortOrder is not supported by current database schema. Please run server migrations (db:migrate)." "sortOrder is not supported by current database schema. Please run server migrations (db:migrate)."
); );
} }
const n = Number(body.sortOrder); const n = Number(body.sortOrder);
if (!Number.isFinite(n)) throw httpError(400, "sortOrder must be a number"); if (!Number.isFinite(n)) throw httpError(400, "sortOrder must be a number");
sets.push(`sort_order=$${i++}`); sets.push(`sort_order=$${i++}`);
params.push(Math.trunc(n)); params.push(Math.trunc(n));
} }
if (Object.prototype.hasOwnProperty.call(body, "url")) { if (Object.prototype.hasOwnProperty.call(body, "url")) {
sets.push(`url=$${i++}`); sets.push(`url=$${i++}`);
params.push(nextUrl); params.push(nextUrl);
sets.push(`url_normalized=$${i++}`); sets.push(`url_normalized=$${i++}`);
params.push(urlNormalized); params.push(urlNormalized);
sets.push(`url_hash=$${i++}`); sets.push(`url_hash=$${i++}`);
params.push(urlHash); params.push(urlHash);
} }
if (sets.length === 0) throw httpError(400, "no fields to update"); if (sets.length === 0) throw httpError(400, "no fields to update");
// If URL changed and collides with another bookmark, auto-merge by keeping the existing row. // If URL changed and collides with another bookmark, auto-merge by keeping the existing row.
if (urlChanged) { if (urlChanged) {
const dup = await app.pg.query( const dup = await app.pg.query(
"select * from bookmarks where user_id=$1 and url_hash=$2 and deleted_at is null and id<>$3 limit 1", "select * from bookmarks where user_id=$1 and url_hash=$2 and deleted_at is null and id<>$3 limit 1",
[userId, urlHash, id] [userId, urlHash, id]
); );
if (dup.rows[0]) { if (dup.rows[0]) {
const targetId = dup.rows[0].id; const targetId = dup.rows[0].id;
const merged = await app.pg.query( const merged = await app.pg.query(
`update bookmarks `update bookmarks
set ${sets.join(", ")}, source='manual', updated_at=now() set ${sets.join(", ")}, source='manual', updated_at=now()
where id=$${i++} and user_id=$${i} where id=$${i++} and user_id=$${i}
returning *`, returning *`,
[...params, targetId, userId] [...params, targetId, userId]
); );
await app.pg.query( await app.pg.query(
"update bookmarks set deleted_at=now(), updated_at=now() where id=$1 and user_id=$2", "update bookmarks set deleted_at=now(), updated_at=now() where id=$1 and user_id=$2",
[id, userId] [id, userId]
); );
return bookmarkRowToDto(merged.rows[0]); return bookmarkRowToDto(merged.rows[0]);
} }
} }
params.push(id, userId); params.push(id, userId);
const res = await app.pg.query( const res = await app.pg.query(
`update bookmarks `update bookmarks
set ${sets.join(", ")}, source='manual', updated_at=now() set ${sets.join(", ")}, source='manual', updated_at=now()
where id=$${i++} and user_id=$${i} where id=$${i++} and user_id=$${i}
returning *`, returning *`,
params params
); );
return bookmarkRowToDto(res.rows[0]); return bookmarkRowToDto(res.rows[0]);
} }
); );
app.delete( app.delete(
"/bookmarks/:id", "/bookmarks/:id",
{ preHandler: [app.authenticate] }, { preHandler: [app.authenticate] },
async (req) => { async (req) => {
const userId = req.user.sub; const userId = req.user.sub;
const id = req.params?.id; const id = req.params?.id;
const res = await app.pg.query( const res = await app.pg.query(
"update bookmarks set deleted_at=now(), updated_at=now() where id=$1 and user_id=$2 and deleted_at is null returning *", "update bookmarks set deleted_at=now(), updated_at=now() where id=$1 and user_id=$2 and deleted_at is null returning *",
[id, userId] [id, userId]
); );
if (!res.rows[0]) throw httpError(404, "bookmark not found"); if (!res.rows[0]) throw httpError(404, "bookmark not found");
return res.rows.map(bookmarkRowToDto)[0]; return res.rows.map(bookmarkRowToDto)[0];
} }
); );
} }

View File

@@ -1,199 +1,199 @@
import { httpError } from "../lib/httpErrors.js"; import { httpError } from "../lib/httpErrors.js";
import { folderRowToDto } from "../lib/rows.js"; import { folderRowToDto } from "../lib/rows.js";
export async function foldersRoutes(app) { export async function foldersRoutes(app) {
app.get( app.get(
"/folders", "/folders",
{ preHandler: [app.authenticate] }, { preHandler: [app.authenticate] },
async (req) => { async (req) => {
const userId = req.user.sub; const userId = req.user.sub;
const orderBy = app.features?.folderSortOrder const orderBy = app.features?.folderSortOrder
? "parent_id nulls first, sort_order asc, name asc" ? "parent_id nulls first, sort_order asc, name asc"
: "parent_id nulls first, name asc"; : "parent_id nulls first, name asc";
const res = await app.pg.query( const res = await app.pg.query(
`select * from bookmark_folders where user_id=$1 order by ${orderBy}`, `select * from bookmark_folders where user_id=$1 order by ${orderBy}`,
[userId] [userId]
); );
return res.rows.map(folderRowToDto); return res.rows.map(folderRowToDto);
} }
); );
app.post( app.post(
"/folders", "/folders",
{ preHandler: [app.authenticate] }, { preHandler: [app.authenticate] },
async (req) => { async (req) => {
const userId = req.user.sub; const userId = req.user.sub;
const { parentId, name, visibility } = req.body || {}; const { parentId, name, visibility } = req.body || {};
await app.pg.query("begin"); await app.pg.query("begin");
try { try {
// Move bookmarks in this folder back to root (so they remain visible). // Move bookmarks in this folder back to root (so they remain visible).
await app.pg.query( await app.pg.query(
"update bookmarks set folder_id=null, updated_at=now() where user_id=$1 and folder_id=$2 and deleted_at is null", "update bookmarks set folder_id=null, updated_at=now() where user_id=$1 and folder_id=$2 and deleted_at is null",
[userId, id] [userId, id]
); );
// Lift child folders to root. // Lift child folders to root.
await app.pg.query( await app.pg.query(
"update bookmark_folders set parent_id=null, updated_at=now() where user_id=$1 and parent_id=$2", "update bookmark_folders set parent_id=null, updated_at=now() where user_id=$1 and parent_id=$2",
[userId, id] [userId, id]
); );
const res = await app.pg.query( const res = await app.pg.query(
"delete from bookmark_folders where id=$1 and user_id=$2 returning id", "delete from bookmark_folders where id=$1 and user_id=$2 returning id",
[id, userId] [id, userId]
); );
if (!res.rows[0]) throw httpError(404, "folder not found"); if (!res.rows[0]) throw httpError(404, "folder not found");
await app.pg.query("commit"); await app.pg.query("commit");
} catch (e) { } catch (e) {
await app.pg.query("rollback"); await app.pg.query("rollback");
throw e; throw e;
} }
return { ok: true }; return { ok: true };
const res = app.features?.folderSortOrder const res = app.features?.folderSortOrder
? await app.pg.query( ? await app.pg.query(
`insert into bookmark_folders (user_id, parent_id, name, visibility, sort_order) `insert into bookmark_folders (user_id, parent_id, name, visibility, sort_order)
values ( values (
$1, $1,
$2, $2,
$3, $3,
$4, $4,
(select coalesce(max(sort_order), -1) + 1 from bookmark_folders where user_id=$1 and parent_id is not distinct from $2) (select coalesce(max(sort_order), -1) + 1 from bookmark_folders where user_id=$1 and parent_id is not distinct from $2)
) )
returning *`, returning *`,
[userId, parent, name, visibility] [userId, parent, name, visibility]
) )
: await app.pg.query( : await app.pg.query(
`insert into bookmark_folders (user_id, parent_id, name, visibility) `insert into bookmark_folders (user_id, parent_id, name, visibility)
values ($1, $2, $3, $4) values ($1, $2, $3, $4)
returning *`, returning *`,
[userId, parent, name, visibility] [userId, parent, name, visibility]
); );
return folderRowToDto(res.rows[0]); return folderRowToDto(res.rows[0]);
} }
); );
app.post( app.post(
"/folders/reorder", "/folders/reorder",
{ preHandler: [app.authenticate] }, { preHandler: [app.authenticate] },
async (req) => { async (req) => {
if (!app.features?.folderSortOrder) { if (!app.features?.folderSortOrder) {
throw httpError( throw httpError(
409, 409,
"folder sort order is not supported by current database schema. Please run server migrations (db:migrate)." "folder sort order is not supported by current database schema. Please run server migrations (db:migrate)."
); );
} }
const userId = req.user.sub; const userId = req.user.sub;
const { parentId, orderedIds } = req.body || {}; const { parentId, orderedIds } = req.body || {};
const parent = parentId ?? null; const parent = parentId ?? null;
if (!Array.isArray(orderedIds) || orderedIds.length === 0) { if (!Array.isArray(orderedIds) || orderedIds.length === 0) {
throw httpError(400, "orderedIds required"); throw httpError(400, "orderedIds required");
} }
const siblings = await app.pg.query( const siblings = await app.pg.query(
"select id from bookmark_folders where user_id=$1 and parent_id is not distinct from $2", "select id from bookmark_folders where user_id=$1 and parent_id is not distinct from $2",
[userId, parent] [userId, parent]
); );
const siblingIds = siblings.rows.map((r) => r.id); const siblingIds = siblings.rows.map((r) => r.id);
// ensure same set // ensure same set
const want = new Set(orderedIds); const want = new Set(orderedIds);
if (want.size !== orderedIds.length) throw httpError(400, "orderedIds must be unique"); if (want.size !== orderedIds.length) throw httpError(400, "orderedIds must be unique");
if (siblingIds.length !== orderedIds.length) throw httpError(400, "orderedIds must include all sibling folders"); if (siblingIds.length !== orderedIds.length) throw httpError(400, "orderedIds must include all sibling folders");
for (const id of siblingIds) { for (const id of siblingIds) {
if (!want.has(id)) throw httpError(400, "orderedIds must include all sibling folders"); if (!want.has(id)) throw httpError(400, "orderedIds must include all sibling folders");
} }
await app.pg.query("begin"); await app.pg.query("begin");
try { try {
for (let i = 0; i < orderedIds.length; i++) { for (let i = 0; i < orderedIds.length; i++) {
await app.pg.query( await app.pg.query(
"update bookmark_folders set sort_order=$1, updated_at=now() where id=$2 and user_id=$3", "update bookmark_folders set sort_order=$1, updated_at=now() where id=$2 and user_id=$3",
[i, orderedIds[i], userId] [i, orderedIds[i], userId]
); );
} }
await app.pg.query("commit"); await app.pg.query("commit");
} catch (e) { } catch (e) {
await app.pg.query("rollback"); await app.pg.query("rollback");
throw e; throw e;
} }
return { ok: true }; return { ok: true };
} }
); );
app.patch( app.patch(
"/folders/:id", "/folders/:id",
{ preHandler: [app.authenticate] }, { preHandler: [app.authenticate] },
async (req) => { async (req) => {
const userId = req.user.sub; const userId = req.user.sub;
const id = req.params?.id; const id = req.params?.id;
const body = req.body || {}; const body = req.body || {};
const existing = await app.pg.query( const existing = await app.pg.query(
"select * from bookmark_folders where id=$1 and user_id=$2", "select * from bookmark_folders where id=$1 and user_id=$2",
[id, userId] [id, userId]
); );
if (!existing.rows[0]) throw httpError(404, "folder not found"); if (!existing.rows[0]) throw httpError(404, "folder not found");
const sets = []; const sets = [];
const params = []; const params = [];
let i = 1; let i = 1;
if (Object.prototype.hasOwnProperty.call(body, "parentId")) { if (Object.prototype.hasOwnProperty.call(body, "parentId")) {
sets.push(`parent_id=$${i++}`); sets.push(`parent_id=$${i++}`);
params.push(body.parentId ?? null); params.push(body.parentId ?? null);
} }
if (Object.prototype.hasOwnProperty.call(body, "name")) { if (Object.prototype.hasOwnProperty.call(body, "name")) {
const name = String(body.name || "").trim(); const name = String(body.name || "").trim();
if (!name) throw httpError(400, "name required"); if (!name) throw httpError(400, "name required");
sets.push(`name=$${i++}`); sets.push(`name=$${i++}`);
params.push(name); params.push(name);
} }
if (Object.prototype.hasOwnProperty.call(body, "visibility")) { if (Object.prototype.hasOwnProperty.call(body, "visibility")) {
if (!body.visibility) throw httpError(400, "visibility required"); if (!body.visibility) throw httpError(400, "visibility required");
sets.push(`visibility=$${i++}`); sets.push(`visibility=$${i++}`);
params.push(body.visibility); params.push(body.visibility);
} }
if (Object.prototype.hasOwnProperty.call(body, "sortOrder")) { if (Object.prototype.hasOwnProperty.call(body, "sortOrder")) {
if (!app.features?.folderSortOrder) { if (!app.features?.folderSortOrder) {
throw httpError( throw httpError(
409, 409,
"sortOrder is not supported by current database schema. Please run server migrations (db:migrate)." "sortOrder is not supported by current database schema. Please run server migrations (db:migrate)."
); );
} }
const n = Number(body.sortOrder); const n = Number(body.sortOrder);
if (!Number.isFinite(n)) throw httpError(400, "sortOrder must be a number"); if (!Number.isFinite(n)) throw httpError(400, "sortOrder must be a number");
sets.push(`sort_order=$${i++}`); sets.push(`sort_order=$${i++}`);
params.push(Math.trunc(n)); params.push(Math.trunc(n));
} }
if (sets.length === 0) throw httpError(400, "no fields to update"); if (sets.length === 0) throw httpError(400, "no fields to update");
params.push(id, userId); params.push(id, userId);
const res = await app.pg.query( const res = await app.pg.query(
`update bookmark_folders set ${sets.join(", ")}, updated_at=now() where id=$${i++} and user_id=$${i} returning *`, `update bookmark_folders set ${sets.join(", ")}, updated_at=now() where id=$${i++} and user_id=$${i} returning *`,
params params
); );
return folderRowToDto(res.rows[0]); return folderRowToDto(res.rows[0]);
} }
); );
app.delete( app.delete(
"/folders/:id", "/folders/:id",
{ preHandler: [app.authenticate] }, { preHandler: [app.authenticate] },
async (req) => { async (req) => {
const userId = req.user.sub; const userId = req.user.sub;
const id = req.params?.id; const id = req.params?.id;
const res = await app.pg.query( const res = await app.pg.query(
"delete from bookmark_folders where id=$1 and user_id=$2 returning id", "delete from bookmark_folders where id=$1 and user_id=$2 returning id",
[id, userId] [id, userId]
); );
if (!res.rows[0]) throw httpError(404, "folder not found"); if (!res.rows[0]) throw httpError(404, "folder not found");
return { ok: true }; return { ok: true };
} }
); );
} }

View File

@@ -1,131 +1,131 @@
import { computeUrlHash, normalizeUrl } from "@browser-bookmark/shared"; import { computeUrlHash, normalizeUrl } from "@browser-bookmark/shared";
import { parseNetscapeBookmarkHtmlNode, buildNetscapeBookmarkHtml } from "../lib/bookmarkHtmlNode.js"; import { parseNetscapeBookmarkHtmlNode, buildNetscapeBookmarkHtml } from "../lib/bookmarkHtmlNode.js";
export async function importExportRoutes(app) { export async function importExportRoutes(app) {
app.post( app.post(
"/bookmarks/import/html", "/bookmarks/import/html",
{ preHandler: [app.authenticate] }, { preHandler: [app.authenticate] },
async (req) => { async (req) => {
const userId = req.user.sub; const userId = req.user.sub;
const file = await req.file(); const file = await req.file();
if (!file) return { imported: 0, merged: 0 }; if (!file) return { imported: 0, merged: 0 };
const chunks = []; const chunks = [];
for await (const c of file.file) chunks.push(c); for await (const c of file.file) chunks.push(c);
const html = Buffer.concat(chunks).toString("utf8"); const html = Buffer.concat(chunks).toString("utf8");
const parsed = parseNetscapeBookmarkHtmlNode(html); const parsed = parseNetscapeBookmarkHtmlNode(html);
// Flatten folders (no nesting): dedupe/merge by folder name for this user. // Flatten folders (no nesting): dedupe/merge by folder name for this user.
const normName = (s) => String(s || "").replace(/\s+/g, " ").trim().toLowerCase(); const normName = (s) => String(s || "").replace(/\s+/g, " ").trim().toLowerCase();
const existingFolders = await app.pg.query( const existingFolders = await app.pg.query(
"select id, name from bookmark_folders where user_id=$1", "select id, name from bookmark_folders where user_id=$1",
[userId] [userId]
); );
const folderIdByName = new Map( const folderIdByName = new Map(
existingFolders.rows.map((r) => [normName(r.name), r.id]) existingFolders.rows.map((r) => [normName(r.name), r.id])
); );
const tempIdToFolderName = new Map( const tempIdToFolderName = new Map(
(parsed.folders || []).map((f) => [f.tempId, f.name]) (parsed.folders || []).map((f) => [f.tempId, f.name])
); );
const tempToDbId = new Map(); const tempToDbId = new Map();
for (const f of parsed.folders || []) { for (const f of parsed.folders || []) {
const key = normName(f.name); const key = normName(f.name);
if (!key) continue; if (!key) continue;
let id = folderIdByName.get(key); let id = folderIdByName.get(key);
if (!id) { if (!id) {
const res = app.features?.folderSortOrder const res = app.features?.folderSortOrder
? await app.pg.query( ? await app.pg.query(
`insert into bookmark_folders (user_id, parent_id, name, visibility, sort_order) `insert into bookmark_folders (user_id, parent_id, name, visibility, sort_order)
values ( values (
$1, $1,
null, null,
$2, $2,
'private', 'private',
(select coalesce(max(sort_order), -1) + 1 from bookmark_folders where user_id=$1 and parent_id is null) (select coalesce(max(sort_order), -1) + 1 from bookmark_folders where user_id=$1 and parent_id is null)
) )
returning id`, returning id`,
[userId, f.name] [userId, f.name]
) )
: await app.pg.query( : await app.pg.query(
`insert into bookmark_folders (user_id, parent_id, name, visibility) `insert into bookmark_folders (user_id, parent_id, name, visibility)
values ($1, null, $2, 'private') values ($1, null, $2, 'private')
returning id`, returning id`,
[userId, f.name] [userId, f.name]
); );
id = res.rows[0].id; id = res.rows[0].id;
folderIdByName.set(key, id); folderIdByName.set(key, id);
} }
tempToDbId.set(f.tempId, id); tempToDbId.set(f.tempId, id);
} }
let imported = 0; let imported = 0;
let merged = 0; let merged = 0;
for (const b of parsed.bookmarks) { for (const b of parsed.bookmarks) {
// Map bookmark's folder via folder name (flattened). // Map bookmark's folder via folder name (flattened).
let folderId = null; let folderId = null;
if (b.parentTempId) { if (b.parentTempId) {
const fname = tempIdToFolderName.get(b.parentTempId); const fname = tempIdToFolderName.get(b.parentTempId);
const key = normName(fname); const key = normName(fname);
folderId = key ? (folderIdByName.get(key) || tempToDbId.get(b.parentTempId) || null) : null; folderId = key ? (folderIdByName.get(key) || tempToDbId.get(b.parentTempId) || null) : null;
} }
const urlNormalized = normalizeUrl(b.url); const urlNormalized = normalizeUrl(b.url);
const urlHash = computeUrlHash(urlNormalized); const urlHash = computeUrlHash(urlNormalized);
const existing = await app.pg.query( const existing = await app.pg.query(
"select id from bookmarks where user_id=$1 and url_hash=$2 and deleted_at is null limit 1", "select id from bookmarks where user_id=$1 and url_hash=$2 and deleted_at is null limit 1",
[userId, urlHash] [userId, urlHash]
); );
if (existing.rows[0]) { if (existing.rows[0]) {
await app.pg.query( await app.pg.query(
`update bookmarks `update bookmarks
set title=$1, url=$2, url_normalized=$3, folder_id=$4, source='import', updated_at=now() set title=$1, url=$2, url_normalized=$3, folder_id=$4, source='import', updated_at=now()
where id=$5`, where id=$5`,
[b.title || "", b.url || "", urlNormalized, folderId, existing.rows[0].id] [b.title || "", b.url || "", urlNormalized, folderId, existing.rows[0].id]
); );
merged++; merged++;
} else { } else {
await app.pg.query( await app.pg.query(
`insert into bookmarks (user_id, folder_id, title, url, url_normalized, url_hash, visibility, source) `insert into bookmarks (user_id, folder_id, title, url, url_normalized, url_hash, visibility, source)
values ($1, $2, $3, $4, $5, $6, 'private', 'import')`, values ($1, $2, $3, $4, $5, $6, 'private', 'import')`,
[userId, folderId, b.title || "", b.url || "", urlNormalized, urlHash] [userId, folderId, b.title || "", b.url || "", urlNormalized, urlHash]
); );
imported++; imported++;
} }
} }
return { imported, merged }; return { imported, merged };
} }
); );
app.get( app.get(
"/bookmarks/export/html", "/bookmarks/export/html",
{ preHandler: [app.authenticate] }, { preHandler: [app.authenticate] },
async (req, reply) => { async (req, reply) => {
const userId = req.user.sub; const userId = req.user.sub;
const folders = await app.pg.query( const folders = await app.pg.query(
"select id, parent_id, name from bookmark_folders where user_id=$1 order by name", "select id, parent_id, name from bookmark_folders where user_id=$1 order by name",
[userId] [userId]
); );
const bookmarks = await app.pg.query( const bookmarks = await app.pg.query(
"select folder_id, title, url from bookmarks where user_id=$1 and deleted_at is null order by title", "select folder_id, title, url from bookmarks where user_id=$1 and deleted_at is null order by title",
[userId] [userId]
); );
const html = buildNetscapeBookmarkHtml({ const html = buildNetscapeBookmarkHtml({
folders: folders.rows.map((r) => ({ id: r.id, parentId: r.parent_id, name: r.name })), folders: folders.rows.map((r) => ({ id: r.id, parentId: r.parent_id, name: r.name })),
bookmarks: bookmarks.rows.map((r) => ({ folderId: r.folder_id, title: r.title, url: r.url })) bookmarks: bookmarks.rows.map((r) => ({ folderId: r.folder_id, title: r.title, url: r.url }))
}); });
reply.type("text/html; charset=utf-8"); reply.type("text/html; charset=utf-8");
return html; return html;
} }
); );
} }

View File

@@ -1,162 +1,162 @@
import { computeUrlHash, normalizeUrl } from "@browser-bookmark/shared"; import { computeUrlHash, normalizeUrl } from "@browser-bookmark/shared";
function toDate(v) { function toDate(v) {
if (!v) return null; if (!v) return null;
const d = new Date(v); const d = new Date(v);
return Number.isNaN(d.getTime()) ? null : d; return Number.isNaN(d.getTime()) ? null : d;
} }
export async function syncRoutes(app) { export async function syncRoutes(app) {
app.post( app.post(
"/sync/push", "/sync/push",
{ preHandler: [app.authenticate] }, { preHandler: [app.authenticate] },
async (req) => { async (req) => {
const userId = req.user.sub; const userId = req.user.sub;
const { bookmarks = [], folders = [] } = req.body || {}; const { bookmarks = [], folders = [] } = req.body || {};
// folders: upsert by id with LWW // folders: upsert by id with LWW
for (const f of folders) { for (const f of folders) {
const incomingUpdatedAt = toDate(f.updatedAt) || new Date(); const incomingUpdatedAt = toDate(f.updatedAt) || new Date();
const existing = await app.pg.query( const existing = await app.pg.query(
"select id, updated_at from bookmark_folders where id=$1 and user_id=$2", "select id, updated_at from bookmark_folders where id=$1 and user_id=$2",
[f.id, userId] [f.id, userId]
); );
if (!existing.rows[0]) { if (!existing.rows[0]) {
await app.pg.query( await app.pg.query(
`insert into bookmark_folders (id, user_id, parent_id, name, visibility, updated_at) `insert into bookmark_folders (id, user_id, parent_id, name, visibility, updated_at)
values ($1, $2, $3, $4, $5, $6)`, values ($1, $2, $3, $4, $5, $6)`,
[f.id, userId, f.parentId ?? null, f.name || "", f.visibility || "private", incomingUpdatedAt] [f.id, userId, f.parentId ?? null, f.name || "", f.visibility || "private", incomingUpdatedAt]
); );
} else { } else {
const serverUpdatedAt = new Date(existing.rows[0].updated_at); const serverUpdatedAt = new Date(existing.rows[0].updated_at);
if (incomingUpdatedAt > serverUpdatedAt) { if (incomingUpdatedAt > serverUpdatedAt) {
await app.pg.query( await app.pg.query(
`update bookmark_folders `update bookmark_folders
set parent_id=$1, name=$2, visibility=$3, updated_at=$4 set parent_id=$1, name=$2, visibility=$3, updated_at=$4
where id=$5 and user_id=$6`, where id=$5 and user_id=$6`,
[f.parentId ?? null, f.name || "", f.visibility || "private", incomingUpdatedAt, f.id, userId] [f.parentId ?? null, f.name || "", f.visibility || "private", incomingUpdatedAt, f.id, userId]
); );
} }
} }
} }
// bookmarks: upsert by id with LWW; keep urlHash normalized // bookmarks: upsert by id with LWW; keep urlHash normalized
for (const b of bookmarks) { for (const b of bookmarks) {
const incomingUpdatedAt = toDate(b.updatedAt) || new Date(); const incomingUpdatedAt = toDate(b.updatedAt) || new Date();
const incomingDeletedAt = toDate(b.deletedAt); const incomingDeletedAt = toDate(b.deletedAt);
const urlNormalized = normalizeUrl(b.url || ""); const urlNormalized = normalizeUrl(b.url || "");
const urlHash = computeUrlHash(urlNormalized); const urlHash = computeUrlHash(urlNormalized);
const existing = await app.pg.query( const existing = await app.pg.query(
"select id, updated_at from bookmarks where id=$1 and user_id=$2", "select id, updated_at from bookmarks where id=$1 and user_id=$2",
[b.id, userId] [b.id, userId]
); );
if (!existing.rows[0]) { if (!existing.rows[0]) {
await app.pg.query( await app.pg.query(
`insert into bookmarks ( `insert into bookmarks (
id, user_id, folder_id, title, url, url_normalized, url_hash, visibility, source, updated_at, deleted_at id, user_id, folder_id, title, url, url_normalized, url_hash, visibility, source, updated_at, deleted_at
) values ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11)`, ) values ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11)`,
[ [
b.id, b.id,
userId, userId,
b.folderId ?? null, b.folderId ?? null,
b.title || "", b.title || "",
b.url || "", b.url || "",
urlNormalized, urlNormalized,
urlHash, urlHash,
b.visibility || "private", b.visibility || "private",
b.source || "manual", b.source || "manual",
incomingUpdatedAt, incomingUpdatedAt,
incomingDeletedAt incomingDeletedAt
] ]
); );
} else { } else {
const serverUpdatedAt = new Date(existing.rows[0].updated_at); const serverUpdatedAt = new Date(existing.rows[0].updated_at);
if (incomingUpdatedAt > serverUpdatedAt) { if (incomingUpdatedAt > serverUpdatedAt) {
await app.pg.query( await app.pg.query(
`update bookmarks `update bookmarks
set folder_id=$1, title=$2, url=$3, url_normalized=$4, url_hash=$5, visibility=$6, source=$7, updated_at=$8, deleted_at=$9 set folder_id=$1, title=$2, url=$3, url_normalized=$4, url_hash=$5, visibility=$6, source=$7, updated_at=$8, deleted_at=$9
where id=$10 and user_id=$11`, where id=$10 and user_id=$11`,
[ [
b.folderId ?? null, b.folderId ?? null,
b.title || "", b.title || "",
b.url || "", b.url || "",
urlNormalized, urlNormalized,
urlHash, urlHash,
b.visibility || "private", b.visibility || "private",
b.source || "manual", b.source || "manual",
incomingUpdatedAt, incomingUpdatedAt,
incomingDeletedAt, incomingDeletedAt,
b.id, b.id,
userId userId
] ]
); );
} }
} }
} }
return { ok: true }; return { ok: true };
} }
); );
app.get( app.get(
"/sync/pull", "/sync/pull",
{ preHandler: [app.authenticate] }, { preHandler: [app.authenticate] },
async (req) => { async (req) => {
const userId = req.user.sub; const userId = req.user.sub;
const since = toDate(req.query?.since); const since = toDate(req.query?.since);
const paramsFolders = [userId]; const paramsFolders = [userId];
let whereFolders = "where user_id=$1"; let whereFolders = "where user_id=$1";
if (since) { if (since) {
paramsFolders.push(since); paramsFolders.push(since);
whereFolders += ` and updated_at > $${paramsFolders.length}`; whereFolders += ` and updated_at > $${paramsFolders.length}`;
} }
const paramsBookmarks = [userId]; const paramsBookmarks = [userId];
let whereBookmarks = "where user_id=$1"; let whereBookmarks = "where user_id=$1";
if (since) { if (since) {
paramsBookmarks.push(since); paramsBookmarks.push(since);
whereBookmarks += ` and updated_at > $${paramsBookmarks.length}`; whereBookmarks += ` and updated_at > $${paramsBookmarks.length}`;
} }
const foldersRes = await app.pg.query( const foldersRes = await app.pg.query(
`select id, user_id, parent_id, name, visibility, created_at, updated_at from bookmark_folders ${whereFolders}`, `select id, user_id, parent_id, name, visibility, created_at, updated_at from bookmark_folders ${whereFolders}`,
paramsFolders paramsFolders
); );
const bookmarksRes = await app.pg.query( const bookmarksRes = await app.pg.query(
`select id, user_id, folder_id, title, url, url_normalized, url_hash, visibility, source, updated_at, deleted_at from bookmarks ${whereBookmarks}`, `select id, user_id, folder_id, title, url, url_normalized, url_hash, visibility, source, updated_at, deleted_at from bookmarks ${whereBookmarks}`,
paramsBookmarks paramsBookmarks
); );
return { return {
folders: foldersRes.rows.map((r) => ({ folders: foldersRes.rows.map((r) => ({
id: r.id, id: r.id,
userId: r.user_id, userId: r.user_id,
parentId: r.parent_id, parentId: r.parent_id,
name: r.name, name: r.name,
visibility: r.visibility, visibility: r.visibility,
createdAt: r.created_at, createdAt: r.created_at,
updatedAt: r.updated_at updatedAt: r.updated_at
})), })),
bookmarks: bookmarksRes.rows.map((r) => ({ bookmarks: bookmarksRes.rows.map((r) => ({
id: r.id, id: r.id,
userId: r.user_id, userId: r.user_id,
folderId: r.folder_id, folderId: r.folder_id,
title: r.title, title: r.title,
url: r.url, url: r.url,
urlNormalized: r.url_normalized, urlNormalized: r.url_normalized,
urlHash: r.url_hash, urlHash: r.url_hash,
visibility: r.visibility, visibility: r.visibility,
source: r.source, source: r.source,
updatedAt: r.updated_at, updatedAt: r.updated_at,
deletedAt: r.deleted_at deletedAt: r.deleted_at
})), })),
serverTime: new Date().toISOString() serverTime: new Date().toISOString()
}; };
} }
); );
} }

View File

@@ -1,12 +1,12 @@
export default [ export default [
{ {
files: ["**/*.js", "**/*.vue"], files: ["**/*.js", "**/*.vue"],
languageOptions: { languageOptions: {
ecmaVersion: 2024, ecmaVersion: 2024,
sourceType: "module" sourceType: "module"
}, },
rules: { rules: {
"no-unused-vars": ["error", { "argsIgnorePattern": "^_" }] "no-unused-vars": ["error", { "argsIgnorePattern": "^_" }]
} }
} }
]; ];

View File

@@ -11,7 +11,7 @@
"lint": "eslint ." "lint": "eslint ."
}, },
"dependencies": { "dependencies": {
"@browser-bookmark/shared": "0.1.0", "@browser-bookmark/shared": "file:../../packages/shared",
"sortablejs": "^1.15.6", "sortablejs": "^1.15.6",
"vue": "^3.5.24", "vue": "^3.5.24",
"vue-router": "^4.5.1" "vue-router": "^4.5.1"

View File

@@ -1,108 +1,108 @@
<script setup> <script setup>
import { onBeforeUnmount, onMounted } from "vue"; import { onBeforeUnmount, onMounted } from "vue";
const props = defineProps({ const props = defineProps({
modelValue: { type: Boolean, default: false }, modelValue: { type: Boolean, default: false },
title: { type: String, default: "" }, title: { type: String, default: "" },
maxWidth: { type: String, default: "720px" } maxWidth: { type: String, default: "720px" }
}); });
const emit = defineEmits(["update:modelValue"]); const emit = defineEmits(["update:modelValue"]);
function close() { function close() {
emit("update:modelValue", false); emit("update:modelValue", false);
} }
function onKeydown(e) { function onKeydown(e) {
if (e.key === "Escape") close(); if (e.key === "Escape") close();
} }
onMounted(() => { onMounted(() => {
window.addEventListener("keydown", onKeydown); window.addEventListener("keydown", onKeydown);
}); });
onBeforeUnmount(() => { onBeforeUnmount(() => {
window.removeEventListener("keydown", onKeydown); window.removeEventListener("keydown", onKeydown);
}); });
</script> </script>
<template> <template>
<teleport to="body"> <teleport to="body">
<div v-if="modelValue" class="bb-modalOverlay" role="dialog" aria-modal="true"> <div v-if="modelValue" class="bb-modalOverlay" role="dialog" aria-modal="true">
<div class="bb-modalBackdrop" @click="close" /> <div class="bb-modalBackdrop" @click="close" />
<div class="bb-modalPanel" :style="{ maxWidth }"> <div class="bb-modalPanel" :style="{ maxWidth }">
<div class="bb-modalHeader"> <div class="bb-modalHeader">
<div class="bb-modalTitle">{{ title }}</div> <div class="bb-modalTitle">{{ title }}</div>
<button type="button" class="bb-modalClose" @click="close" aria-label="关闭">×</button> <button type="button" class="bb-modalClose" @click="close" aria-label="关闭">×</button>
</div> </div>
<div class="bb-modalBody"> <div class="bb-modalBody">
<slot /> <slot />
</div> </div>
</div> </div>
</div> </div>
</teleport> </teleport>
</template> </template>
<style scoped> <style scoped>
.bb-modalOverlay { .bb-modalOverlay {
position: fixed; position: fixed;
inset: 0; inset: 0;
z-index: 2147483500; z-index: 2147483500;
display: grid; display: grid;
place-items: center; place-items: center;
padding: 18px; padding: 18px;
} }
.bb-modalBackdrop { .bb-modalBackdrop {
position: absolute; position: absolute;
inset: 0; inset: 0;
background: rgba(15, 23, 42, 0.35); background: rgba(15, 23, 42, 0.35);
backdrop-filter: blur(6px); backdrop-filter: blur(6px);
} }
.bb-modalPanel { .bb-modalPanel {
position: relative; position: relative;
width: min(100%, var(--bb-modal-max, 720px)); width: min(100%, var(--bb-modal-max, 720px));
max-height: min(84vh, 860px); max-height: min(84vh, 860px);
overflow: auto; overflow: auto;
border-radius: 18px; border-radius: 18px;
border: 1px solid rgba(255,255,255,0.65); border: 1px solid rgba(255,255,255,0.65);
background: rgba(255,255,255,0.82); background: rgba(255,255,255,0.82);
backdrop-filter: blur(14px); backdrop-filter: blur(14px);
box-shadow: 0 18px 60px rgba(15, 23, 42, 0.18); box-shadow: 0 18px 60px rgba(15, 23, 42, 0.18);
} }
.bb-modalHeader { .bb-modalHeader {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
gap: 10px; gap: 10px;
padding: 12px 14px; padding: 12px 14px;
border-bottom: 1px solid rgba(15, 23, 42, 0.08); border-bottom: 1px solid rgba(15, 23, 42, 0.08);
} }
.bb-modalTitle { .bb-modalTitle {
font-weight: 900; font-weight: 900;
color: var(--bb-text); color: var(--bb-text);
} }
.bb-modalClose { .bb-modalClose {
width: 34px; width: 34px;
height: 34px; height: 34px;
border-radius: 12px; border-radius: 12px;
border: 1px solid rgba(255,255,255,0.55); border: 1px solid rgba(255,255,255,0.55);
background: rgba(255,255,255,0.45); background: rgba(255,255,255,0.45);
cursor: pointer; cursor: pointer;
font-size: 20px; font-size: 20px;
line-height: 1; line-height: 1;
color: rgba(15, 23, 42, 0.72); color: rgba(15, 23, 42, 0.72);
} }
.bb-modalClose:hover { .bb-modalClose:hover {
background: rgba(255,255,255,0.75); background: rgba(255,255,255,0.75);
} }
.bb-modalBody { .bb-modalBody {
padding: 14px; padding: 14px;
} }
</style> </style>

View File

@@ -1,153 +1,153 @@
<script setup> <script setup>
import { computed, nextTick, onBeforeUnmount, onMounted, ref } from "vue"; import { computed, nextTick, onBeforeUnmount, onMounted, ref } from "vue";
const props = defineProps({ const props = defineProps({
modelValue: { type: [String, Number, Boolean, Object, null], default: null }, modelValue: { type: [String, Number, Boolean, Object, null], default: null },
options: { type: Array, required: true }, options: { type: Array, required: true },
disabled: { type: Boolean, default: false }, disabled: { type: Boolean, default: false },
placeholder: { type: String, default: "请选择" }, placeholder: { type: String, default: "请选择" },
size: { type: String, default: "md" } // md | sm size: { type: String, default: "md" } // md | sm
}); });
const emit = defineEmits(["update:modelValue"]); const emit = defineEmits(["update:modelValue"]);
const rootEl = ref(null); const rootEl = ref(null);
const triggerEl = ref(null); const triggerEl = ref(null);
const menuEl = ref(null); const menuEl = ref(null);
const open = ref(false); const open = ref(false);
const menuStyle = ref({ left: "0px", top: "0px", width: "0px" }); const menuStyle = ref({ left: "0px", top: "0px", width: "0px" });
const selected = computed(() => props.options.find((o) => Object.is(o.value, props.modelValue)) || null); const selected = computed(() => props.options.find((o) => Object.is(o.value, props.modelValue)) || null);
const label = computed(() => selected.value?.label ?? ""); const label = computed(() => selected.value?.label ?? "");
function close() { function close() {
open.value = false; open.value = false;
} }
async function updateMenuPosition() { async function updateMenuPosition() {
const el = triggerEl.value; const el = triggerEl.value;
if (!el) return; if (!el) return;
const rect = el.getBoundingClientRect(); const rect = el.getBoundingClientRect();
const gap = 8; const gap = 8;
menuStyle.value = { menuStyle.value = {
left: `${Math.round(rect.left)}px`, left: `${Math.round(rect.left)}px`,
top: `${Math.round(rect.bottom + gap)}px`, top: `${Math.round(rect.bottom + gap)}px`,
width: `${Math.round(rect.width)}px` width: `${Math.round(rect.width)}px`
}; };
} }
async function openMenu() { async function openMenu() {
if (props.disabled) return; if (props.disabled) return;
open.value = true; open.value = true;
await nextTick(); await nextTick();
await updateMenuPosition(); await updateMenuPosition();
} }
function toggle() { function toggle() {
if (props.disabled) return; if (props.disabled) return;
if (open.value) close(); if (open.value) close();
else openMenu(); else openMenu();
} }
function choose(value, isDisabled) { function choose(value, isDisabled) {
if (props.disabled || isDisabled) return; if (props.disabled || isDisabled) return;
emit("update:modelValue", value); emit("update:modelValue", value);
close(); close();
} }
function onKeydownTrigger(e) { function onKeydownTrigger(e) {
if (props.disabled) return; if (props.disabled) return;
if (e.key === "Enter" || e.key === " ") { if (e.key === "Enter" || e.key === " ") {
e.preventDefault(); e.preventDefault();
toggle(); toggle();
} }
if (e.key === "Escape") { if (e.key === "Escape") {
e.preventDefault(); e.preventDefault();
close(); close();
} }
} }
function onDocPointerDown(e) { function onDocPointerDown(e) {
const el = rootEl.value; const el = rootEl.value;
const menu = menuEl.value; const menu = menuEl.value;
if (!el) return; if (!el) return;
if (el.contains(e.target)) return; if (el.contains(e.target)) return;
if (menu && menu.contains(e.target)) return; if (menu && menu.contains(e.target)) return;
close(); close();
} }
function onViewportChange() { function onViewportChange() {
if (!open.value) return; if (!open.value) return;
updateMenuPosition(); updateMenuPosition();
} }
onMounted(() => { onMounted(() => {
document.addEventListener("pointerdown", onDocPointerDown); document.addEventListener("pointerdown", onDocPointerDown);
window.addEventListener("resize", onViewportChange); window.addEventListener("resize", onViewportChange);
// capture scroll events from any scroll container // capture scroll events from any scroll container
window.addEventListener("scroll", onViewportChange, true); window.addEventListener("scroll", onViewportChange, true);
}); });
onBeforeUnmount(() => { onBeforeUnmount(() => {
document.removeEventListener("pointerdown", onDocPointerDown); document.removeEventListener("pointerdown", onDocPointerDown);
window.removeEventListener("resize", onViewportChange); window.removeEventListener("resize", onViewportChange);
window.removeEventListener("scroll", onViewportChange, true); window.removeEventListener("scroll", onViewportChange, true);
}); });
</script> </script>
<template> <template>
<div <div
ref="rootEl" ref="rootEl"
class="bb-selectWrap" class="bb-selectWrap"
:class="[ :class="[
size === 'sm' ? 'bb-selectWrap--sm' : '', size === 'sm' ? 'bb-selectWrap--sm' : '',
open ? 'is-open' : '' open ? 'is-open' : ''
]" ]"
> >
<button <button
type="button" type="button"
class="bb-selectTrigger" class="bb-selectTrigger"
:disabled="disabled" :disabled="disabled"
:aria-expanded="open ? 'true' : 'false'" :aria-expanded="open ? 'true' : 'false'"
@click="toggle" @click="toggle"
@keydown="onKeydownTrigger" @keydown="onKeydownTrigger"
ref="triggerEl" ref="triggerEl"
> >
<span class="bb-selectValue" :class="!label ? 'bb-selectPlaceholder' : ''"> <span class="bb-selectValue" :class="!label ? 'bb-selectPlaceholder' : ''">
{{ label || placeholder }} {{ label || placeholder }}
</span> </span>
<span class="bb-selectChevron" aria-hidden="true"> <span class="bb-selectChevron" aria-hidden="true">
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M6 9l6 6 6-6" /> <path d="M6 9l6 6 6-6" />
</svg> </svg>
</span> </span>
</button> </button>
<teleport to="body"> <teleport to="body">
<div <div
v-if="open" v-if="open"
ref="menuEl" ref="menuEl"
class="bb-selectMenu bb-selectMenu--portal" class="bb-selectMenu bb-selectMenu--portal"
role="listbox" role="listbox"
:style="menuStyle" :style="menuStyle"
> >
<button <button
v-for="(o, idx) in options" v-for="(o, idx) in options"
:key="idx" :key="idx"
type="button" type="button"
class="bb-selectOption" class="bb-selectOption"
:class="[ :class="[
Object.is(o.value, modelValue) ? 'is-selected' : '', Object.is(o.value, modelValue) ? 'is-selected' : '',
o.disabled ? 'is-disabled' : '' o.disabled ? 'is-disabled' : ''
]" ]"
role="option" role="option"
:aria-selected="Object.is(o.value, modelValue) ? 'true' : 'false'" :aria-selected="Object.is(o.value, modelValue) ? 'true' : 'false'"
@click="choose(o.value, o.disabled)" @click="choose(o.value, o.disabled)"
> >
<span class="bb-selectOptionLabel">{{ o.label }}</span> <span class="bb-selectOptionLabel">{{ o.label }}</span>
</button> </button>
</div> </div>
</teleport> </teleport>
</div> </div>
</template> </template>

View File

@@ -1,90 +1,90 @@
import { ref } from "vue"; import { ref } from "vue";
const BASE_URL = import.meta.env.VITE_SERVER_BASE_URL || "http://localhost:3001"; const BASE_URL = import.meta.env.VITE_SERVER_BASE_URL || "http://localhost:3001";
export const tokenRef = ref(localStorage.getItem("bb_token") || ""); export const tokenRef = ref(localStorage.getItem("bb_token") || "");
export const userRef = ref(null); export const userRef = ref(null);
let mePromise = null; let mePromise = null;
export function getToken() { export function getToken() {
return tokenRef.value || ""; return tokenRef.value || "";
} }
export function setToken(token) { export function setToken(token) {
const next = token || ""; const next = token || "";
tokenRef.value = next; tokenRef.value = next;
if (next) localStorage.setItem("bb_token", next); if (next) localStorage.setItem("bb_token", next);
else localStorage.removeItem("bb_token"); else localStorage.removeItem("bb_token");
// reset cached user when auth changes // reset cached user when auth changes
userRef.value = null; userRef.value = null;
mePromise = null; mePromise = null;
} }
// Keep auth state in sync across tabs. // Keep auth state in sync across tabs.
if (typeof window !== "undefined") { if (typeof window !== "undefined") {
window.addEventListener("storage", (e) => { window.addEventListener("storage", (e) => {
if (e.key === "bb_token") { if (e.key === "bb_token") {
tokenRef.value = e.newValue || ""; tokenRef.value = e.newValue || "";
userRef.value = null; userRef.value = null;
mePromise = null; mePromise = null;
} }
}); });
} }
export async function ensureMe() { export async function ensureMe() {
const token = getToken(); const token = getToken();
if (!token) { if (!token) {
userRef.value = null; userRef.value = null;
mePromise = null; mePromise = null;
return null; return null;
} }
if (userRef.value) return userRef.value; if (userRef.value) return userRef.value;
if (mePromise) return mePromise; if (mePromise) return mePromise;
mePromise = (async () => { mePromise = (async () => {
try { try {
const me = await apiFetch("/auth/me", { method: "GET" }); const me = await apiFetch("/auth/me", { method: "GET" });
userRef.value = me; userRef.value = me;
return me; return me;
} catch { } catch {
// token may be invalid/expired // token may be invalid/expired
userRef.value = null; userRef.value = null;
return null; return null;
} finally { } finally {
mePromise = null; mePromise = null;
} }
})(); })();
return mePromise; return mePromise;
} }
export async function apiFetch(path, options = {}) { export async function apiFetch(path, options = {}) {
const headers = new Headers(options.headers || {}); const headers = new Headers(options.headers || {});
if (!headers.has("Accept")) headers.set("Accept", "application/json"); if (!headers.has("Accept")) headers.set("Accept", "application/json");
if (!(options.body instanceof FormData) && options.body != null) { if (!(options.body instanceof FormData) && options.body != null) {
headers.set("Content-Type", "application/json"); headers.set("Content-Type", "application/json");
} }
const token = getToken(); const token = getToken();
if (token) headers.set("Authorization", `Bearer ${token}`); if (token) headers.set("Authorization", `Bearer ${token}`);
const res = await fetch(`${BASE_URL}${path}`, { ...options, headers }); const res = await fetch(`${BASE_URL}${path}`, { ...options, headers });
const contentType = res.headers.get("content-type") || ""; const contentType = res.headers.get("content-type") || "";
const isJson = contentType.includes("application/json"); const isJson = contentType.includes("application/json");
const payload = isJson ? await res.json().catch(() => null) : await res.text().catch(() => ""); const payload = isJson ? await res.json().catch(() => null) : await res.text().catch(() => "");
if (!res.ok) { if (!res.ok) {
const message = payload?.message || `HTTP ${res.status}`; const message = payload?.message || `HTTP ${res.status}`;
const err = new Error(message); const err = new Error(message);
err.status = res.status; err.status = res.status;
err.payload = payload; err.payload = payload;
throw err; throw err;
} }
return payload; return payload;
} }

View File

@@ -1,290 +1,290 @@
import { computeUrlHash, normalizeUrl, parseNetscapeBookmarkHtml } from "@browser-bookmark/shared"; import { computeUrlHash, normalizeUrl, parseNetscapeBookmarkHtml } from "@browser-bookmark/shared";
const KEY = "bb_local_state_v1"; const KEY = "bb_local_state_v1";
function nowIso() { function nowIso() {
return new Date().toISOString(); return new Date().toISOString();
} }
function ensureBookmarkHashes(bookmark) { function ensureBookmarkHashes(bookmark) {
if (!bookmark) return bookmark; if (!bookmark) return bookmark;
const url = bookmark.url || ""; const url = bookmark.url || "";
const urlNormalized = bookmark.urlNormalized || normalizeUrl(url); const urlNormalized = bookmark.urlNormalized || normalizeUrl(url);
const urlHash = bookmark.urlHash || computeUrlHash(urlNormalized); const urlHash = bookmark.urlHash || computeUrlHash(urlNormalized);
bookmark.urlNormalized = urlNormalized; bookmark.urlNormalized = urlNormalized;
bookmark.urlHash = urlHash; bookmark.urlHash = urlHash;
return bookmark; return bookmark;
} }
export function loadLocalState() { export function loadLocalState() {
try { try {
const parsed = JSON.parse(localStorage.getItem(KEY) || "") || { folders: [], bookmarks: [] }; const parsed = JSON.parse(localStorage.getItem(KEY) || "") || { folders: [], bookmarks: [] };
parsed.folders = parsed.folders || []; parsed.folders = parsed.folders || [];
parsed.bookmarks = (parsed.bookmarks || []).map((b) => ensureBookmarkHashes(b)); parsed.bookmarks = (parsed.bookmarks || []).map((b) => ensureBookmarkHashes(b));
return parsed; return parsed;
} catch { } catch {
return { folders: [], bookmarks: [] }; return { folders: [], bookmarks: [] };
} }
} }
export function saveLocalState(state) { export function saveLocalState(state) {
localStorage.setItem(KEY, JSON.stringify(state)); localStorage.setItem(KEY, JSON.stringify(state));
} }
export function listLocalBookmarks({ includeDeleted = false } = {}) { export function listLocalBookmarks({ includeDeleted = false } = {}) {
const state = loadLocalState(); const state = loadLocalState();
const all = state.bookmarks || []; const all = state.bookmarks || [];
return includeDeleted ? all : all.filter((b) => !b.deletedAt); return includeDeleted ? all : all.filter((b) => !b.deletedAt);
} }
export function upsertLocalBookmark({ title, url, visibility = "public", folderId = null, source = "manual" }) { export function upsertLocalBookmark({ title, url, visibility = "public", folderId = null, source = "manual" }) {
const state = loadLocalState(); const state = loadLocalState();
const now = nowIso(); const now = nowIso();
const urlNormalized = normalizeUrl(url || ""); const urlNormalized = normalizeUrl(url || "");
const urlHash = computeUrlHash(urlNormalized); const urlHash = computeUrlHash(urlNormalized);
// Dedupe: same urlHash and not deleted -> update LWW // Dedupe: same urlHash and not deleted -> update LWW
const existing = (state.bookmarks || []).find((b) => !b.deletedAt && (b.urlHash || "") === urlHash); const existing = (state.bookmarks || []).find((b) => !b.deletedAt && (b.urlHash || "") === urlHash);
if (existing) { if (existing) {
ensureBookmarkHashes(existing); ensureBookmarkHashes(existing);
existing.title = title || existing.title; existing.title = title || existing.title;
existing.url = url || existing.url; existing.url = url || existing.url;
existing.urlNormalized = urlNormalized; existing.urlNormalized = urlNormalized;
existing.urlHash = urlHash; existing.urlHash = urlHash;
existing.visibility = visibility; existing.visibility = visibility;
existing.folderId = folderId; existing.folderId = folderId;
existing.source = source; existing.source = source;
existing.updatedAt = now; existing.updatedAt = now;
saveLocalState(state); saveLocalState(state);
return existing; return existing;
} }
const bookmark = { const bookmark = {
id: crypto.randomUUID(), id: crypto.randomUUID(),
userId: null, userId: null,
folderId, folderId,
title, title,
url, url,
urlNormalized, urlNormalized,
urlHash, urlHash,
visibility, visibility,
source, source,
updatedAt: now, updatedAt: now,
deletedAt: null deletedAt: null
}; };
state.bookmarks.unshift(bookmark); state.bookmarks.unshift(bookmark);
saveLocalState(state); saveLocalState(state);
return bookmark; return bookmark;
} }
export function markLocalDeleted(id) { export function markLocalDeleted(id) {
const state = loadLocalState(); const state = loadLocalState();
const now = nowIso(); const now = nowIso();
const item = state.bookmarks.find((b) => b.id === id); const item = state.bookmarks.find((b) => b.id === id);
if (item) { if (item) {
item.deletedAt = now; item.deletedAt = now;
item.updatedAt = now; item.updatedAt = now;
saveLocalState(state); saveLocalState(state);
} }
} }
export function patchLocalBookmark(id, patch) { export function patchLocalBookmark(id, patch) {
const state = loadLocalState(); const state = loadLocalState();
const now = nowIso(); const now = nowIso();
const item = state.bookmarks.find((b) => b.id === id); const item = state.bookmarks.find((b) => b.id === id);
if (!item) return null; if (!item) return null;
if (patch.url !== undefined) { if (patch.url !== undefined) {
const nextUrl = patch.url || ""; const nextUrl = patch.url || "";
const nextNormalized = normalizeUrl(nextUrl); const nextNormalized = normalizeUrl(nextUrl);
const nextHash = computeUrlHash(nextNormalized); const nextHash = computeUrlHash(nextNormalized);
const target = (state.bookmarks || []).find((b) => !b.deletedAt && b.id !== id && (b.urlHash || "") === nextHash); const target = (state.bookmarks || []).find((b) => !b.deletedAt && b.id !== id && (b.urlHash || "") === nextHash);
if (target) { if (target) {
// Merge: write changes to target and soft-delete current // Merge: write changes to target and soft-delete current
ensureBookmarkHashes(target); ensureBookmarkHashes(target);
target.title = patch.title !== undefined ? patch.title : item.title; target.title = patch.title !== undefined ? patch.title : item.title;
target.url = nextUrl; target.url = nextUrl;
target.urlNormalized = nextNormalized; target.urlNormalized = nextNormalized;
target.urlHash = nextHash; target.urlHash = nextHash;
if (patch.folderId !== undefined) target.folderId = patch.folderId; if (patch.folderId !== undefined) target.folderId = patch.folderId;
if (patch.visibility !== undefined) target.visibility = patch.visibility; if (patch.visibility !== undefined) target.visibility = patch.visibility;
target.updatedAt = now; target.updatedAt = now;
item.deletedAt = now; item.deletedAt = now;
item.updatedAt = now; item.updatedAt = now;
saveLocalState(state); saveLocalState(state);
return target; return target;
} }
item.url = nextUrl; item.url = nextUrl;
item.urlNormalized = nextNormalized; item.urlNormalized = nextNormalized;
item.urlHash = nextHash; item.urlHash = nextHash;
} }
if (patch.title !== undefined) item.title = patch.title; if (patch.title !== undefined) item.title = patch.title;
if (patch.folderId !== undefined) item.folderId = patch.folderId; if (patch.folderId !== undefined) item.folderId = patch.folderId;
if (patch.visibility !== undefined) item.visibility = patch.visibility; if (patch.visibility !== undefined) item.visibility = patch.visibility;
item.updatedAt = now; item.updatedAt = now;
saveLocalState(state); saveLocalState(state);
return item; return item;
} }
export function deleteLocalFolder(folderId) { export function deleteLocalFolder(folderId) {
const state = loadLocalState(); const state = loadLocalState();
const now = nowIso(); const now = nowIso();
state.folders = (state.folders || []).filter((f) => f.id !== folderId); state.folders = (state.folders || []).filter((f) => f.id !== folderId);
state.bookmarks = (state.bookmarks || []).map((b) => { state.bookmarks = (state.bookmarks || []).map((b) => {
if (b.deletedAt) return b; if (b.deletedAt) return b;
if ((b.folderId ?? null) !== (folderId ?? null)) return b; if ((b.folderId ?? null) !== (folderId ?? null)) return b;
return { ...ensureBookmarkHashes(b), folderId: null, updatedAt: now }; return { ...ensureBookmarkHashes(b), folderId: null, updatedAt: now };
}); });
saveLocalState(state); saveLocalState(state);
} }
export function importLocalFromNetscapeHtml(html, { visibility = "public" } = {}) { export function importLocalFromNetscapeHtml(html, { visibility = "public" } = {}) {
const parsed = parseNetscapeBookmarkHtml(html || ""); const parsed = parseNetscapeBookmarkHtml(html || "");
const state = loadLocalState(); const state = loadLocalState();
const now = nowIso(); const now = nowIso();
state.folders = state.folders || []; state.folders = state.folders || [];
state.bookmarks = state.bookmarks || []; state.bookmarks = state.bookmarks || [];
// Flatten folders (no nesting): dedupe local folders by name. // Flatten folders (no nesting): dedupe local folders by name.
const normName = (s) => String(s || "").replace(/\s+/g, " ").trim(); const normName = (s) => String(s || "").replace(/\s+/g, " ").trim();
const normKey = (s) => normName(s).toLowerCase(); const normKey = (s) => normName(s).toLowerCase();
const existingFolderByName = new Map( const existingFolderByName = new Map(
(state.folders || []) (state.folders || [])
.filter((f) => !f.deletedAt) .filter((f) => !f.deletedAt)
.map((f) => [normKey(f.name), f]) .map((f) => [normKey(f.name), f])
); );
const tempIdToFolderName = new Map( const tempIdToFolderName = new Map(
(parsed.folders || []).map((f) => [String(f.id), f.name]) (parsed.folders || []).map((f) => [String(f.id), f.name])
); );
const folderIdByName = new Map( const folderIdByName = new Map(
(state.folders || []) (state.folders || [])
.filter((f) => !f.deletedAt) .filter((f) => !f.deletedAt)
.map((f) => [normKey(f.name), f.id]) .map((f) => [normKey(f.name), f.id])
); );
for (const f of parsed.folders || []) { for (const f of parsed.folders || []) {
const name = normName(f.name); const name = normName(f.name);
const key = normKey(name); const key = normKey(name);
if (!key) continue; if (!key) continue;
let id = folderIdByName.get(key); let id = folderIdByName.get(key);
if (!id) { if (!id) {
const created = { const created = {
id: crypto.randomUUID(), id: crypto.randomUUID(),
userId: null, userId: null,
parentId: null, parentId: null,
name, name,
visibility: "private", visibility: "private",
sortOrder: (state.folders || []).filter((x) => !x.deletedAt && (x.parentId ?? null) === null).length, sortOrder: (state.folders || []).filter((x) => !x.deletedAt && (x.parentId ?? null) === null).length,
updatedAt: now, updatedAt: now,
deletedAt: null deletedAt: null
}; };
state.folders.push(created); state.folders.push(created);
existingFolderByName.set(key, created); existingFolderByName.set(key, created);
folderIdByName.set(key, created.id); folderIdByName.set(key, created.id);
id = created.id; id = created.id;
} }
} }
const existingByHash = new Map( const existingByHash = new Map(
(state.bookmarks || []) (state.bookmarks || [])
.filter((b) => !b.deletedAt) .filter((b) => !b.deletedAt)
.map((b) => { .map((b) => {
const fixed = ensureBookmarkHashes(b); const fixed = ensureBookmarkHashes(b);
return [fixed.urlHash, fixed]; return [fixed.urlHash, fixed];
}) })
); );
let imported = 0; let imported = 0;
let merged = 0; let merged = 0;
for (const b of parsed.bookmarks || []) { for (const b of parsed.bookmarks || []) {
const url = (b.url || "").trim(); const url = (b.url || "").trim();
if (!url) continue; if (!url) continue;
const title = (b.title || "").trim() || url; const title = (b.title || "").trim() || url;
const folderTempId = b.parentFolderId ?? null; const folderTempId = b.parentFolderId ?? null;
const folderName = folderTempId ? tempIdToFolderName.get(String(folderTempId)) : null; const folderName = folderTempId ? tempIdToFolderName.get(String(folderTempId)) : null;
const folderId = folderName ? (folderIdByName.get(normKey(folderName)) ?? null) : null; const folderId = folderName ? (folderIdByName.get(normKey(folderName)) ?? null) : null;
const urlNormalized = normalizeUrl(url); const urlNormalized = normalizeUrl(url);
const urlHash = computeUrlHash(urlNormalized); const urlHash = computeUrlHash(urlNormalized);
const existing = existingByHash.get(urlHash); const existing = existingByHash.get(urlHash);
if (existing) { if (existing) {
existing.title = title || existing.title; existing.title = title || existing.title;
existing.url = url; existing.url = url;
existing.urlNormalized = urlNormalized; existing.urlNormalized = urlNormalized;
existing.urlHash = urlHash; existing.urlHash = urlHash;
existing.visibility = visibility; existing.visibility = visibility;
existing.folderId = folderId; existing.folderId = folderId;
existing.source = "import"; existing.source = "import";
existing.updatedAt = now; existing.updatedAt = now;
merged++; merged++;
continue; continue;
} }
const created = { const created = {
id: crypto.randomUUID(), id: crypto.randomUUID(),
userId: null, userId: null,
folderId, folderId,
title, title,
url, url,
urlNormalized, urlNormalized,
urlHash, urlHash,
visibility, visibility,
source: "import", source: "import",
updatedAt: now, updatedAt: now,
deletedAt: null deletedAt: null
}; };
state.bookmarks.unshift(created); state.bookmarks.unshift(created);
existingByHash.set(urlHash, created); existingByHash.set(urlHash, created);
imported++; imported++;
} }
saveLocalState(state); saveLocalState(state);
return { imported, merged }; return { imported, merged };
} }
export function exportLocalToNetscapeHtml(bookmarks) { export function exportLocalToNetscapeHtml(bookmarks) {
const safe = (s) => String(s || "").replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;"); const safe = (s) => String(s || "").replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
const lines = []; const lines = [];
lines.push("<!DOCTYPE NETSCAPE-Bookmark-file-1>"); lines.push("<!DOCTYPE NETSCAPE-Bookmark-file-1>");
lines.push('<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8">'); lines.push('<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8">');
lines.push("<TITLE>Bookmarks</TITLE>"); lines.push("<TITLE>Bookmarks</TITLE>");
lines.push("<H1>Bookmarks</H1>"); lines.push("<H1>Bookmarks</H1>");
lines.push("<DL><p>"); lines.push("<DL><p>");
for (const b of bookmarks || []) { for (const b of bookmarks || []) {
if (b.deletedAt) continue; if (b.deletedAt) continue;
lines.push(` <DT><A HREF="${safe(b.url)}">${safe(b.title || b.url)}</A>`); lines.push(` <DT><A HREF="${safe(b.url)}">${safe(b.title || b.url)}</A>`);
} }
lines.push("</DL><p>"); lines.push("</DL><p>");
return lines.join("\n"); return lines.join("\n");
} }
export function mergeLocalToUser(userId) { export function mergeLocalToUser(userId) {
const state = loadLocalState(); const state = loadLocalState();
return { return {
folders: state.folders.map((f) => ({ ...f, userId })), folders: state.folders.map((f) => ({ ...f, userId })),
bookmarks: state.bookmarks.map((b) => ({ ...ensureBookmarkHashes(b), userId })) bookmarks: state.bookmarks.map((b) => ({ ...ensureBookmarkHashes(b), userId }))
}; };
} }
export function clearLocalState() { export function clearLocalState() {
saveLocalState({ folders: [], bookmarks: [] }); saveLocalState({ folders: [], bookmarks: [] });
} }

View File

@@ -1,435 +1,435 @@
<script setup> <script setup>
import { computed, onBeforeUnmount, onMounted, ref, watch } from "vue"; import { computed, onBeforeUnmount, onMounted, ref, watch } from "vue";
import { apiFetch, ensureMe, userRef } from "../lib/api"; import { apiFetch, ensureMe, userRef } from "../lib/api";
import BbModal from "../components/BbModal.vue"; import BbModal from "../components/BbModal.vue";
const loadingUsers = ref(false); const loadingUsers = ref(false);
const usersError = ref(""); const usersError = ref("");
const users = ref([]); const users = ref([]);
const selectedUserId = ref(""); const selectedUserId = ref("");
const selectedUser = computed(() => users.value.find((u) => u.id === selectedUserId.value) || null); const selectedUser = computed(() => users.value.find((u) => u.id === selectedUserId.value) || null);
const q = ref(""); const q = ref("");
const loadingBookmarks = ref(false); const loadingBookmarks = ref(false);
const bookmarksError = ref(""); const bookmarksError = ref("");
const bookmarks = ref([]); const bookmarks = ref([]);
const loadingFolders = ref(false); const loadingFolders = ref(false);
const foldersError = ref(""); const foldersError = ref("");
const folders = ref([]); const folders = ref([]);
const openFolderIds = ref(new Set()); const openFolderIds = ref(new Set());
const isAdmin = computed(() => userRef.value?.role === "admin"); const isAdmin = computed(() => userRef.value?.role === "admin");
const confirmOpen = ref(false); const confirmOpen = ref(false);
const confirmTitle = ref("请确认"); const confirmTitle = ref("请确认");
const confirmMessage = ref(""); const confirmMessage = ref("");
const confirmOkText = ref("确定"); const confirmOkText = ref("确定");
const confirmDanger = ref(false); const confirmDanger = ref(false);
let confirmResolve = null; let confirmResolve = null;
function askConfirm(message, { title = "请确认", okText = "确定", danger = false } = {}) { function askConfirm(message, { title = "请确认", okText = "确定", danger = false } = {}) {
confirmTitle.value = title; confirmTitle.value = title;
confirmMessage.value = message; confirmMessage.value = message;
confirmOkText.value = okText; confirmOkText.value = okText;
confirmDanger.value = danger; confirmDanger.value = danger;
confirmOpen.value = true; confirmOpen.value = true;
return new Promise((resolve) => { return new Promise((resolve) => {
confirmResolve = resolve; confirmResolve = resolve;
}); });
} }
function resolveConfirm(result) { function resolveConfirm(result) {
const resolve = confirmResolve; const resolve = confirmResolve;
confirmResolve = null; confirmResolve = null;
confirmOpen.value = false; confirmOpen.value = false;
if (resolve) resolve(Boolean(result)); if (resolve) resolve(Boolean(result));
} }
function onConfirmModalUpdate(v) { function onConfirmModalUpdate(v) {
if (!v) resolveConfirm(false); if (!v) resolveConfirm(false);
else confirmOpen.value = true; else confirmOpen.value = true;
} }
async function loadUsers() { async function loadUsers() {
loadingUsers.value = true; loadingUsers.value = true;
usersError.value = ""; usersError.value = "";
try { try {
users.value = await apiFetch("/admin/users"); users.value = await apiFetch("/admin/users");
if (!selectedUserId.value && users.value.length) selectedUserId.value = users.value[0].id; if (!selectedUserId.value && users.value.length) selectedUserId.value = users.value[0].id;
} catch (e) { } catch (e) {
usersError.value = e.message || String(e); usersError.value = e.message || String(e);
} finally { } finally {
loadingUsers.value = false; loadingUsers.value = false;
} }
} }
async function loadFolders() { async function loadFolders() {
if (!selectedUserId.value) return; if (!selectedUserId.value) return;
loadingFolders.value = true; loadingFolders.value = true;
foldersError.value = ""; foldersError.value = "";
try { try {
folders.value = await apiFetch(`/admin/users/${selectedUserId.value}/folders`); folders.value = await apiFetch(`/admin/users/${selectedUserId.value}/folders`);
} catch (e) { } catch (e) {
foldersError.value = e.message || String(e); foldersError.value = e.message || String(e);
} finally { } finally {
loadingFolders.value = false; loadingFolders.value = false;
} }
} }
async function loadBookmarks() { async function loadBookmarks() {
if (!selectedUserId.value) return; if (!selectedUserId.value) return;
loadingBookmarks.value = true; loadingBookmarks.value = true;
bookmarksError.value = ""; bookmarksError.value = "";
try { try {
const qs = q.value.trim() ? `?q=${encodeURIComponent(q.value.trim())}` : ""; const qs = q.value.trim() ? `?q=${encodeURIComponent(q.value.trim())}` : "";
bookmarks.value = await apiFetch(`/admin/users/${selectedUserId.value}/bookmarks${qs}`); bookmarks.value = await apiFetch(`/admin/users/${selectedUserId.value}/bookmarks${qs}`);
} catch (e) { } catch (e) {
bookmarksError.value = e.message || String(e); bookmarksError.value = e.message || String(e);
} finally { } finally {
loadingBookmarks.value = false; loadingBookmarks.value = false;
} }
} }
let searchTimer = 0; let searchTimer = 0;
watch( watch(
() => q.value, () => q.value,
() => { () => {
window.clearTimeout(searchTimer); window.clearTimeout(searchTimer);
searchTimer = window.setTimeout(() => { searchTimer = window.setTimeout(() => {
loadBookmarks(); loadBookmarks();
}, 200); }, 200);
} }
); );
onBeforeUnmount(() => { onBeforeUnmount(() => {
window.clearTimeout(searchTimer); window.clearTimeout(searchTimer);
}); });
function selectUser(id) { function selectUser(id) {
selectedUserId.value = id; selectedUserId.value = id;
q.value = ""; q.value = "";
folders.value = []; folders.value = [];
bookmarks.value = []; bookmarks.value = [];
openFolderIds.value = new Set(["ROOT"]); openFolderIds.value = new Set(["ROOT"]);
loadFolders(); loadFolders();
loadBookmarks(); loadBookmarks();
} }
function openUrl(url) { function openUrl(url) {
if (!url) return; if (!url) return;
window.open(url, "_blank", "noopener,noreferrer"); window.open(url, "_blank", "noopener,noreferrer");
} }
function buildFolderFlat(list) { function buildFolderFlat(list) {
const byId = new Map((list || []).map((f) => [f.id, f])); const byId = new Map((list || []).map((f) => [f.id, f]));
const children = new Map(); const children = new Map();
for (const f of list || []) { for (const f of list || []) {
const key = f.parentId ?? null; const key = f.parentId ?? null;
if (!children.has(key)) children.set(key, []); if (!children.has(key)) children.set(key, []);
children.get(key).push(f); children.get(key).push(f);
} }
for (const arr of children.values()) { for (const arr of children.values()) {
arr.sort((a, b) => { arr.sort((a, b) => {
const ao = Number.isFinite(a.sortOrder) ? a.sortOrder : 0; const ao = Number.isFinite(a.sortOrder) ? a.sortOrder : 0;
const bo = Number.isFinite(b.sortOrder) ? b.sortOrder : 0; const bo = Number.isFinite(b.sortOrder) ? b.sortOrder : 0;
if (ao !== bo) return ao - bo; if (ao !== bo) return ao - bo;
return String(a.name || "").localeCompare(String(b.name || "")); return String(a.name || "").localeCompare(String(b.name || ""));
}); });
} }
const out = []; const out = [];
const visited = new Set(); const visited = new Set();
function walk(parentId, depth) { function walk(parentId, depth) {
const arr = children.get(parentId ?? null) || []; const arr = children.get(parentId ?? null) || [];
for (const f of arr) { for (const f of arr) {
if (!f?.id || visited.has(f.id)) continue; if (!f?.id || visited.has(f.id)) continue;
visited.add(f.id); visited.add(f.id);
out.push({ folder: f, depth }); out.push({ folder: f, depth });
walk(f.id, depth + 1); walk(f.id, depth + 1);
} }
} }
walk(null, 0); walk(null, 0);
for (const f of list || []) { for (const f of list || []) {
if (f?.id && !visited.has(f.id) && byId.has(f.id)) { if (f?.id && !visited.has(f.id) && byId.has(f.id)) {
out.push({ folder: f, depth: 0 }); out.push({ folder: f, depth: 0 });
visited.add(f.id); visited.add(f.id);
} }
} }
return out; return out;
} }
const folderFlat = computed(() => buildFolderFlat(folders.value)); const folderFlat = computed(() => buildFolderFlat(folders.value));
const bookmarksByFolderId = computed(() => { const bookmarksByFolderId = computed(() => {
const map = new Map(); const map = new Map();
for (const b of bookmarks.value || []) { for (const b of bookmarks.value || []) {
const key = b.folderId ?? null; const key = b.folderId ?? null;
if (!map.has(key)) map.set(key, []); if (!map.has(key)) map.set(key, []);
map.get(key).push(b); map.get(key).push(b);
} }
for (const arr of map.values()) { for (const arr of map.values()) {
arr.sort((a, b) => { arr.sort((a, b) => {
const ao = Number.isFinite(a.sortOrder) ? a.sortOrder : 0; const ao = Number.isFinite(a.sortOrder) ? a.sortOrder : 0;
const bo = Number.isFinite(b.sortOrder) ? b.sortOrder : 0; const bo = Number.isFinite(b.sortOrder) ? b.sortOrder : 0;
if (ao !== bo) return ao - bo; if (ao !== bo) return ao - bo;
return String(b.updatedAt || "").localeCompare(String(a.updatedAt || "")); return String(b.updatedAt || "").localeCompare(String(a.updatedAt || ""));
}); });
} }
return map; return map;
}); });
function folderCount(folderId) { function folderCount(folderId) {
return (bookmarksByFolderId.value.get(folderId ?? null) || []).length; return (bookmarksByFolderId.value.get(folderId ?? null) || []).length;
} }
function toggleFolder(folderId) { function toggleFolder(folderId) {
const set = new Set(openFolderIds.value); const set = new Set(openFolderIds.value);
if (set.has(folderId)) set.delete(folderId); if (set.has(folderId)) set.delete(folderId);
else set.add(folderId); else set.add(folderId);
openFolderIds.value = set; openFolderIds.value = set;
} }
function isFolderOpen(folderId) { function isFolderOpen(folderId) {
return openFolderIds.value.has(folderId); return openFolderIds.value.has(folderId);
} }
async function deleteBookmark(bookmarkId) { async function deleteBookmark(bookmarkId) {
if (!selectedUserId.value) return; if (!selectedUserId.value) return;
if (!(await askConfirm("确定删除该书签?", { title: "删除书签", okText: "删除", danger: true }))) return; if (!(await askConfirm("确定删除该书签?", { title: "删除书签", okText: "删除", danger: true }))) return;
try { try {
await apiFetch(`/admin/users/${selectedUserId.value}/bookmarks/${bookmarkId}`, { method: "DELETE" }); await apiFetch(`/admin/users/${selectedUserId.value}/bookmarks/${bookmarkId}`, { method: "DELETE" });
await loadBookmarks(); await loadBookmarks();
} catch (e) { } catch (e) {
bookmarksError.value = e.message || String(e); bookmarksError.value = e.message || String(e);
} }
} }
async function deleteFolder(folderId) { async function deleteFolder(folderId) {
if (!selectedUserId.value) return; if (!selectedUserId.value) return;
if (!(await askConfirm("确定删除该文件夹?子文件夹会一起删除,文件夹内书签会变成未分组。", { title: "删除文件夹", okText: "删除", danger: true }))) return; if (!(await askConfirm("确定删除该文件夹?子文件夹会一起删除,文件夹内书签会变成未分组。", { title: "删除文件夹", okText: "删除", danger: true }))) return;
try { try {
await apiFetch(`/admin/users/${selectedUserId.value}/folders/${folderId}`, { method: "DELETE" }); await apiFetch(`/admin/users/${selectedUserId.value}/folders/${folderId}`, { method: "DELETE" });
await loadFolders(); await loadFolders();
await loadBookmarks(); await loadBookmarks();
} catch (e) { } catch (e) {
foldersError.value = e.message || String(e); foldersError.value = e.message || String(e);
} }
} }
async function copyToMe(bookmarkId) { async function copyToMe(bookmarkId) {
if (!selectedUserId.value) return; if (!selectedUserId.value) return;
try { try {
await apiFetch(`/admin/users/${selectedUserId.value}/bookmarks/${bookmarkId}/copy-to-me`, { method: "POST" }); await apiFetch(`/admin/users/${selectedUserId.value}/bookmarks/${bookmarkId}/copy-to-me`, { method: "POST" });
} catch (e) { } catch (e) {
bookmarksError.value = e.message || String(e); bookmarksError.value = e.message || String(e);
} }
} }
onMounted(async () => { onMounted(async () => {
await ensureMe(); await ensureMe();
if (!isAdmin.value) return; if (!isAdmin.value) return;
await loadUsers(); await loadUsers();
if (selectedUserId.value) openFolderIds.value = new Set(["ROOT"]); if (selectedUserId.value) openFolderIds.value = new Set(["ROOT"]);
await loadFolders(); await loadFolders();
await loadBookmarks(); await loadBookmarks();
}); });
</script> </script>
<template> <template>
<section class="bb-page"> <section class="bb-page">
<div class="bb-pageHeader"> <div class="bb-pageHeader">
<div> <div>
<h1 style="margin: 0;">管理用户</h1> <h1 style="margin: 0;">管理用户</h1>
<div class="bb-muted" style="margin-top: 4px;">仅管理员可见查看用户列表与其书签</div> <div class="bb-muted" style="margin-top: 4px;">仅管理员可见查看用户列表与其书签</div>
</div> </div>
</div> </div>
<div v-if="!isAdmin" class="bb-empty" style="margin-top: 12px;"> <div v-if="!isAdmin" class="bb-empty" style="margin-top: 12px;">
<div style="font-weight: 900;">无权限</div> <div style="font-weight: 900;">无权限</div>
<div class="bb-muted" style="margin-top: 6px;">当前账号不是管理员</div> <div class="bb-muted" style="margin-top: 6px;">当前账号不是管理员</div>
</div> </div>
<div v-else class="bb-adminGrid" style="margin-top: 12px;"> <div v-else class="bb-adminGrid" style="margin-top: 12px;">
<aside class="bb-card"> <aside class="bb-card">
<div class="bb-cardTitle">用户</div> <div class="bb-cardTitle">用户</div>
<div class="bb-muted" style="margin-top: 4px;">点击查看该用户书签</div> <div class="bb-muted" style="margin-top: 4px;">点击查看该用户书签</div>
<p v-if="usersError" class="bb-alert bb-alert--error" style="margin-top: 10px;">{{ usersError }}</p> <p v-if="usersError" class="bb-alert bb-alert--error" style="margin-top: 10px;">{{ usersError }}</p>
<p v-else-if="loadingUsers" class="bb-muted" style="margin-top: 10px;">加载中</p> <p v-else-if="loadingUsers" class="bb-muted" style="margin-top: 10px;">加载中</p>
<div v-else class="bb-adminUserList" style="margin-top: 10px;"> <div v-else class="bb-adminUserList" style="margin-top: 10px;">
<button <button
v-for="u in users" v-for="u in users"
:key="u.id" :key="u.id"
type="button" type="button"
class="bb-adminUser" class="bb-adminUser"
:class="u.id === selectedUserId ? 'is-active' : ''" :class="u.id === selectedUserId ? 'is-active' : ''"
@click="selectUser(u.id)" @click="selectUser(u.id)"
> >
<div class="email">{{ u.email }}</div> <div class="email">{{ u.email }}</div>
<div class="meta">{{ u.role === 'admin' ? '管理员' : '用户' }}</div> <div class="meta">{{ u.role === 'admin' ? '管理员' : '用户' }}</div>
</button> </button>
</div> </div>
</aside> </aside>
<section class="bb-card"> <section class="bb-card">
<div class="bb-row" style="justify-content: space-between; align-items: flex-end;"> <div class="bb-row" style="justify-content: space-between; align-items: flex-end;">
<div> <div>
<div class="bb-cardTitle">书签</div> <div class="bb-cardTitle">书签</div>
<div class="bb-muted" style="margin-top: 4px;"> <div class="bb-muted" style="margin-top: 4px;">
<span v-if="selectedUser">当前{{ selectedUser.email }}</span> <span v-if="selectedUser">当前{{ selectedUser.email }}</span>
<span v-else>请选择一个用户</span> <span v-else>请选择一个用户</span>
</div> </div>
</div> </div>
<div class="bb-row" style="gap: 8px;"> <div class="bb-row" style="gap: 8px;">
<div class="bb-searchWrap" style="min-width: 260px;"> <div class="bb-searchWrap" style="min-width: 260px;">
<input v-model="q" class="bb-input bb-input--sm bb-input--withClear" placeholder="搜索标题/链接" /> <input v-model="q" class="bb-input bb-input--sm bb-input--withClear" placeholder="搜索标题/链接" />
<button v-if="q.trim()" class="bb-searchClear" type="button" aria-label="清空搜索" @click="q = ''">×</button> <button v-if="q.trim()" class="bb-searchClear" type="button" aria-label="清空搜索" @click="q = ''">×</button>
</div> </div>
</div> </div>
</div> </div>
<p v-if="bookmarksError" class="bb-alert bb-alert--error" style="margin-top: 12px;">{{ bookmarksError }}</p> <p v-if="bookmarksError" class="bb-alert bb-alert--error" style="margin-top: 12px;">{{ bookmarksError }}</p>
<p v-else-if="loadingBookmarks" class="bb-muted" style="margin-top: 12px;">加载中</p> <p v-else-if="loadingBookmarks" class="bb-muted" style="margin-top: 12px;">加载中</p>
<p v-if="foldersError" class="bb-alert bb-alert--error" style="margin-top: 12px;">{{ foldersError }}</p> <p v-if="foldersError" class="bb-alert bb-alert--error" style="margin-top: 12px;">{{ foldersError }}</p>
<div v-else-if="(!folders.length && !bookmarks.length)" class="bb-empty" style="margin-top: 12px;"> <div v-else-if="(!folders.length && !bookmarks.length)" class="bb-empty" style="margin-top: 12px;">
<div style="font-weight: 800;">暂无数据</div> <div style="font-weight: 800;">暂无数据</div>
<div class="bb-muted" style="margin-top: 6px;">该用户还没有书签或文件夹</div> <div class="bb-muted" style="margin-top: 6px;">该用户还没有书签或文件夹</div>
</div> </div>
<div v-else class="bb-adminTree" style="margin-top: 12px;"> <div v-else class="bb-adminTree" style="margin-top: 12px;">
<!-- Root group --> <!-- Root group -->
<div class="bb-adminFolder" :class="isFolderOpen('ROOT') ? 'is-open' : ''"> <div class="bb-adminFolder" :class="isFolderOpen('ROOT') ? 'is-open' : ''">
<button type="button" class="bb-adminFolderHeader" @click="toggleFolder('ROOT')"> <button type="button" class="bb-adminFolderHeader" @click="toggleFolder('ROOT')">
<span class="name">未分组</span> <span class="name">未分组</span>
<span class="meta">{{ folderCount(null) }} </span> <span class="meta">{{ folderCount(null) }} </span>
</button> </button>
<div v-if="isFolderOpen('ROOT')" class="bb-adminFolderBody"> <div v-if="isFolderOpen('ROOT')" class="bb-adminFolderBody">
<ul class="bb-adminBookmarks"> <ul class="bb-adminBookmarks">
<li <li
v-for="b in (bookmarksByFolderId.get(null) || [])" v-for="b in (bookmarksByFolderId.get(null) || [])"
:key="b.id" :key="b.id"
class="bb-card bb-card--interactive bb-clickCard" class="bb-card bb-card--interactive bb-clickCard"
role="link" role="link"
tabindex="0" tabindex="0"
@click="openUrl(b.url)" @click="openUrl(b.url)"
@keydown.enter.prevent="openUrl(b.url)" @keydown.enter.prevent="openUrl(b.url)"
@keydown.space.prevent="openUrl(b.url)" @keydown.space.prevent="openUrl(b.url)"
> >
<div class="bb-bookmarkTitle" style="font-weight: 900; color: var(--bb-text);">{{ b.title }}</div> <div class="bb-bookmarkTitle" style="font-weight: 900; color: var(--bb-text);">{{ b.title }}</div>
<div class="bb-muted" style="overflow-wrap: anywhere; margin-top: 6px;">{{ b.url }}</div> <div class="bb-muted" style="overflow-wrap: anywhere; margin-top: 6px;">{{ b.url }}</div>
<div class="bb-adminActions" style="margin-top: 10px;"> <div class="bb-adminActions" style="margin-top: 10px;">
<button class="bb-btn bb-btn--secondary" type="button" @click.stop="copyToMe(b.id)">复制到我</button> <button class="bb-btn bb-btn--secondary" type="button" @click.stop="copyToMe(b.id)">复制到我</button>
<button class="bb-btn bb-btn--danger" type="button" @click.stop="deleteBookmark(b.id)">删除</button> <button class="bb-btn bb-btn--danger" type="button" @click.stop="deleteBookmark(b.id)">删除</button>
</div> </div>
</li> </li>
</ul> </ul>
</div> </div>
</div> </div>
<!-- Folder tree (flat with indent) --> <!-- Folder tree (flat with indent) -->
<div <div
v-for="x in folderFlat" v-for="x in folderFlat"
:key="x.folder.id" :key="x.folder.id"
class="bb-adminFolder" class="bb-adminFolder"
:class="isFolderOpen(x.folder.id) ? 'is-open' : ''" :class="isFolderOpen(x.folder.id) ? 'is-open' : ''"
:style="{ paddingLeft: `${x.depth * 14}px` }" :style="{ paddingLeft: `${x.depth * 14}px` }"
> >
<div class="bb-adminFolderHeaderRow"> <div class="bb-adminFolderHeaderRow">
<button type="button" class="bb-adminFolderHeader" @click="toggleFolder(x.folder.id)"> <button type="button" class="bb-adminFolderHeader" @click="toggleFolder(x.folder.id)">
<span class="name">{{ x.folder.name }}</span> <span class="name">{{ x.folder.name }}</span>
<span class="meta">{{ folderCount(x.folder.id) }} </span> <span class="meta">{{ folderCount(x.folder.id) }} </span>
</button> </button>
<button class="bb-btn bb-btn--danger bb-adminFolderDel" type="button" @click.stop="deleteFolder(x.folder.id)">删除文件夹</button> <button class="bb-btn bb-btn--danger bb-adminFolderDel" type="button" @click.stop="deleteFolder(x.folder.id)">删除文件夹</button>
</div> </div>
<div v-if="isFolderOpen(x.folder.id)" class="bb-adminFolderBody"> <div v-if="isFolderOpen(x.folder.id)" class="bb-adminFolderBody">
<ul class="bb-adminBookmarks"> <ul class="bb-adminBookmarks">
<li <li
v-for="b in (bookmarksByFolderId.get(x.folder.id) || [])" v-for="b in (bookmarksByFolderId.get(x.folder.id) || [])"
:key="b.id" :key="b.id"
class="bb-card bb-card--interactive bb-clickCard" class="bb-card bb-card--interactive bb-clickCard"
role="link" role="link"
tabindex="0" tabindex="0"
@click="openUrl(b.url)" @click="openUrl(b.url)"
@keydown.enter.prevent="openUrl(b.url)" @keydown.enter.prevent="openUrl(b.url)"
@keydown.space.prevent="openUrl(b.url)" @keydown.space.prevent="openUrl(b.url)"
> >
<div class="bb-bookmarkTitle" style="font-weight: 900; color: var(--bb-text);">{{ b.title }}</div> <div class="bb-bookmarkTitle" style="font-weight: 900; color: var(--bb-text);">{{ b.title }}</div>
<div class="bb-muted" style="overflow-wrap: anywhere; margin-top: 6px;">{{ b.url }}</div> <div class="bb-muted" style="overflow-wrap: anywhere; margin-top: 6px;">{{ b.url }}</div>
<div class="bb-adminActions" style="margin-top: 10px;"> <div class="bb-adminActions" style="margin-top: 10px;">
<button class="bb-btn bb-btn--secondary" type="button" @click.stop="copyToMe(b.id)">复制到我</button> <button class="bb-btn bb-btn--secondary" type="button" @click.stop="copyToMe(b.id)">复制到我</button>
<button class="bb-btn bb-btn--danger" type="button" @click.stop="deleteBookmark(b.id)">删除</button> <button class="bb-btn bb-btn--danger" type="button" @click.stop="deleteBookmark(b.id)">删除</button>
</div> </div>
</li> </li>
</ul> </ul>
</div> </div>
</div> </div>
</div> </div>
</section> </section>
</div> </div>
<BbModal :model-value="confirmOpen" :title="confirmTitle" max-width="520px" @update:model-value="onConfirmModalUpdate"> <BbModal :model-value="confirmOpen" :title="confirmTitle" max-width="520px" @update:model-value="onConfirmModalUpdate">
<div class="bb-muted" style="white-space: pre-wrap; line-height: 1.6;">{{ confirmMessage }}</div> <div class="bb-muted" style="white-space: pre-wrap; line-height: 1.6;">{{ confirmMessage }}</div>
<div class="bb-row" style="justify-content: flex-end; gap: 10px; margin-top: 14px;"> <div class="bb-row" style="justify-content: flex-end; gap: 10px; margin-top: 14px;">
<button class="bb-btn bb-btn--secondary" type="button" @click="resolveConfirm(false)">取消</button> <button class="bb-btn bb-btn--secondary" type="button" @click="resolveConfirm(false)">取消</button>
<button class="bb-btn" :class="confirmDanger ? 'bb-btn--danger' : ''" type="button" @click="resolveConfirm(true)">{{ confirmOkText }}</button> <button class="bb-btn" :class="confirmDanger ? 'bb-btn--danger' : ''" type="button" @click="resolveConfirm(true)">{{ confirmOkText }}</button>
</div> </div>
</BbModal> </BbModal>
</section> </section>
</template> </template>
<style scoped> <style scoped>
.bb-adminGrid { display: grid; grid-template-columns: 1fr; gap: 12px; } .bb-adminGrid { display: grid; grid-template-columns: 1fr; gap: 12px; }
@media (min-width: 960px) { .bb-adminGrid { grid-template-columns: 1.1fr 2fr; } } @media (min-width: 960px) { .bb-adminGrid { grid-template-columns: 1.1fr 2fr; } }
.bb-adminUserList { display: grid; gap: 8px; } .bb-adminUserList { display: grid; gap: 8px; }
.bb-adminUser { .bb-adminUser {
width: 100%; width: 100%;
text-align: left; text-align: left;
padding: 10px 12px; padding: 10px 12px;
border-radius: 16px; border-radius: 16px;
border: 1px solid rgba(255,255,255,0.45); border: 1px solid rgba(255,255,255,0.45);
background: rgba(255,255,255,0.35); background: rgba(255,255,255,0.35);
cursor: pointer; cursor: pointer;
} }
.bb-adminUser:hover { background: rgba(255,255,255,0.6); } .bb-adminUser:hover { background: rgba(255,255,255,0.6); }
.bb-adminUser.is-active { background: rgba(13, 148, 136, 0.12); border-color: rgba(13, 148, 136, 0.22); } .bb-adminUser.is-active { background: rgba(13, 148, 136, 0.12); border-color: rgba(13, 148, 136, 0.22); }
.bb-adminUser .email { font-weight: 800; color: var(--bb-text); } .bb-adminUser .email { font-weight: 800; color: var(--bb-text); }
.bb-adminUser .meta { font-size: 12px; color: rgba(19, 78, 74, 0.72); margin-top: 2px; } .bb-adminUser .meta { font-size: 12px; color: rgba(19, 78, 74, 0.72); margin-top: 2px; }
.bb-adminBookmarks { list-style: none; padding: 0; margin: 12px 0 0; display: grid; grid-template-columns: 1fr; gap: 10px; } .bb-adminBookmarks { list-style: none; padding: 0; margin: 12px 0 0; display: grid; grid-template-columns: 1fr; gap: 10px; }
@media (min-width: 768px) { .bb-adminBookmarks { grid-template-columns: 1fr 1fr; } } @media (min-width: 768px) { .bb-adminBookmarks { grid-template-columns: 1fr 1fr; } }
.bb-clickCard { cursor: pointer; } .bb-clickCard { cursor: pointer; }
.title { font-weight: 900; color: var(--bb-text); } .title { font-weight: 900; color: var(--bb-text); }
.bb-adminFolder { margin-top: 10px; } .bb-adminFolder { margin-top: 10px; }
.bb-adminFolderHeaderRow { display: flex; gap: 10px; align-items: center; } .bb-adminFolderHeaderRow { display: flex; gap: 10px; align-items: center; }
.bb-adminFolderHeader { .bb-adminFolderHeader {
flex: 1; flex: 1;
width: 100%; width: 100%;
text-align: left; text-align: left;
padding: 10px 12px; padding: 10px 12px;
border-radius: 16px; border-radius: 16px;
border: 1px solid rgba(255,255,255,0.45); border: 1px solid rgba(255,255,255,0.45);
background: rgba(255,255,255,0.35); background: rgba(255,255,255,0.35);
cursor: pointer; cursor: pointer;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
gap: 10px; gap: 10px;
} }
.bb-adminFolderHeader:hover { background: rgba(255,255,255,0.6); } .bb-adminFolderHeader:hover { background: rgba(255,255,255,0.6); }
.bb-adminFolderHeader .name { font-weight: 900; color: var(--bb-text); } .bb-adminFolderHeader .name { font-weight: 900; color: var(--bb-text); }
.bb-adminFolderHeader .meta { font-size: 12px; color: rgba(19, 78, 74, 0.72); } .bb-adminFolderHeader .meta { font-size: 12px; color: rgba(19, 78, 74, 0.72); }
.bb-adminFolderBody { margin-top: 10px; } .bb-adminFolderBody { margin-top: 10px; }
.bb-adminFolderDel { white-space: nowrap; } .bb-adminFolderDel { white-space: nowrap; }
.bb-adminActions { display: flex; gap: 8px; flex-wrap: wrap; } .bb-adminActions { display: flex; gap: 8px; flex-wrap: wrap; }
</style> </style>

View File

@@ -1,168 +1,168 @@
<script setup> <script setup>
import { computed, ref } from "vue"; import { computed, ref } from "vue";
import { apiFetch, tokenRef } from "../lib/api"; import { apiFetch, tokenRef } from "../lib/api";
import { import {
exportLocalToNetscapeHtml, exportLocalToNetscapeHtml,
importLocalFromNetscapeHtml, importLocalFromNetscapeHtml,
listLocalBookmarks listLocalBookmarks
} from "../lib/localData"; } from "../lib/localData";
const loggedIn = computed(() => Boolean(tokenRef.value)); const loggedIn = computed(() => Boolean(tokenRef.value));
const file = ref(null); const file = ref(null);
const status = ref(""); const status = ref("");
const error = ref(""); const error = ref("");
const busy = ref(false); const busy = ref(false);
const fileInputEl = ref(null); const fileInputEl = ref(null);
function onFileChange(e) { function onFileChange(e) {
file.value = e?.target?.files?.[0] || null; file.value = e?.target?.files?.[0] || null;
} }
function openFilePicker() { function openFilePicker() {
fileInputEl.value?.click?.(); fileInputEl.value?.click?.();
} }
async function importFile() { async function importFile() {
status.value = ""; status.value = "";
error.value = ""; error.value = "";
if (!file.value) return; if (!file.value) return;
try { try {
busy.value = true; busy.value = true;
if (loggedIn.value) { if (loggedIn.value) {
const fd = new FormData(); const fd = new FormData();
fd.append("file", file.value); fd.append("file", file.value);
const res = await apiFetch("/bookmarks/import/html", { method: "POST", body: fd }); const res = await apiFetch("/bookmarks/import/html", { method: "POST", body: fd });
status.value = `导入完成:新增 ${res.imported},合并 ${res.merged}`; status.value = `导入完成:新增 ${res.imported},合并 ${res.merged}`;
} else { } else {
const text = await file.value.text(); const text = await file.value.text();
const res = importLocalFromNetscapeHtml(text, { visibility: "public" }); const res = importLocalFromNetscapeHtml(text, { visibility: "public" });
status.value = `本地导入完成:新增 ${res.imported},合并 ${res.merged}`; status.value = `本地导入完成:新增 ${res.imported},合并 ${res.merged}`;
} }
} catch (e) { } catch (e) {
error.value = e.message || String(e); error.value = e.message || String(e);
} finally { } finally {
busy.value = false; busy.value = false;
} }
} }
async function exportCloud() { async function exportCloud() {
status.value = ""; status.value = "";
error.value = ""; error.value = "";
try { try {
busy.value = true; busy.value = true;
const html = await apiFetch("/bookmarks/export/html", { const html = await apiFetch("/bookmarks/export/html", {
method: "GET", method: "GET",
headers: { Accept: "text/html" } headers: { Accept: "text/html" }
}); });
const blob = new Blob([html], { type: "text/html;charset=utf-8" }); const blob = new Blob([html], { type: "text/html;charset=utf-8" });
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
const a = document.createElement("a"); const a = document.createElement("a");
a.href = url; a.href = url;
a.download = "bookmarks-cloud.html"; a.download = "bookmarks-cloud.html";
document.body.appendChild(a); document.body.appendChild(a);
a.click(); a.click();
a.remove(); a.remove();
URL.revokeObjectURL(url); URL.revokeObjectURL(url);
status.value = "云端导出完成"; status.value = "云端导出完成";
} catch (e) { } catch (e) {
error.value = e.message || String(e); error.value = e.message || String(e);
} finally { } finally {
busy.value = false; busy.value = false;
} }
} }
async function exportLocal() { async function exportLocal() {
status.value = ""; status.value = "";
error.value = ""; error.value = "";
try { try {
busy.value = true; busy.value = true;
const bookmarks = listLocalBookmarks({ includeDeleted: false }); const bookmarks = listLocalBookmarks({ includeDeleted: false });
const html = exportLocalToNetscapeHtml(bookmarks); const html = exportLocalToNetscapeHtml(bookmarks);
const blob = new Blob([html], { type: "text/html;charset=utf-8" }); const blob = new Blob([html], { type: "text/html;charset=utf-8" });
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
const a = document.createElement("a"); const a = document.createElement("a");
a.href = url; a.href = url;
a.download = "bookmarks.html"; a.download = "bookmarks.html";
document.body.appendChild(a); document.body.appendChild(a);
a.click(); a.click();
a.remove(); a.remove();
URL.revokeObjectURL(url); URL.revokeObjectURL(url);
status.value = `本地导出完成:${bookmarks.length}`; status.value = `本地导出完成:${bookmarks.length}`;
} catch (e) { } catch (e) {
error.value = e.message || String(e); error.value = e.message || String(e);
} finally { } finally {
busy.value = false; busy.value = false;
} }
} }
</script> </script>
<template> <template>
<section class="bb-page"> <section class="bb-page">
<div class="bb-pageHeader"> <div class="bb-pageHeader">
<div> <div>
<h1 style="margin: 0;">导入 / 导出</h1> <h1 style="margin: 0;">导入 / 导出</h1>
<div class="bb-muted" style="margin-top: 4px;">把浏览器书签升级成可同步可复习可分享的知识库</div> <div class="bb-muted" style="margin-top: 4px;">把浏览器书签升级成可同步可复习可分享的知识库</div>
</div> </div>
</div> </div>
<div class="bb-clayCard" style="margin-top: 12px;"> <div class="bb-clayCard" style="margin-top: 12px;">
<div class="bb-cardTitle">导入 Chrome/Edge 书签 HTML</div> <div class="bb-cardTitle">导入 Chrome/Edge 书签 HTML</div>
<div class="bb-cardSub">支持自动去重与合并登录后会写入云端数据库</div> <div class="bb-cardSub">支持自动去重与合并登录后会写入云端数据库</div>
<div class="bb-row" style="margin-top: 12px;"> <div class="bb-row" style="margin-top: 12px;">
<input <input
ref="fileInputEl" ref="fileInputEl"
class="bb-fileInput" class="bb-fileInput"
type="file" type="file"
accept="text/html,.html" accept="text/html,.html"
@change="onFileChange" @change="onFileChange"
/> />
<button class="bb-btn bb-btn--secondary" :disabled="busy" @click="openFilePicker">选择 HTML 文件</button> <button class="bb-btn bb-btn--secondary" :disabled="busy" @click="openFilePicker">选择 HTML 文件</button>
<div class="bb-fileName" :class="!file ? 'is-empty' : ''"> <div class="bb-fileName" :class="!file ? 'is-empty' : ''">
{{ file ? file.name : "未选择文件" }} {{ file ? file.name : "未选择文件" }}
</div> </div>
<button class="bb-btn" :disabled="busy || !file" @click="importFile">开始导入</button> <button class="bb-btn" :disabled="busy || !file" @click="importFile">开始导入</button>
</div> </div>
<p v-if="status" class="bb-alert bb-alert--ok" style="margin-top: 12px;">{{ status }}</p> <p v-if="status" class="bb-alert bb-alert--ok" style="margin-top: 12px;">{{ status }}</p>
<p v-if="error" class="bb-alert bb-alert--error" style="margin-top: 12px;">{{ error }}</p> <p v-if="error" class="bb-alert bb-alert--error" style="margin-top: 12px;">{{ error }}</p>
<p class="bb-muted" style="margin-top: 10px;"> <p class="bb-muted" style="margin-top: 10px;">
未登录导入写入本机 localStorage并自动去重登录导入写入数据库并自动去重合并 未登录导入写入本机 localStorage并自动去重登录导入写入数据库并自动去重合并
</p> </p>
</div> </div>
<div v-if="loggedIn" class="bb-grid2" style="margin-top: 12px;"> <div v-if="loggedIn" class="bb-grid2" style="margin-top: 12px;">
<div class="bb-card bb-card--interactive"> <div class="bb-card bb-card--interactive">
<div class="bb-cardTitle">导出云端</div> <div class="bb-cardTitle">导出云端</div>
<div class="bb-cardSub">保留文件夹层级已携带登录态导出</div> <div class="bb-cardSub">保留文件夹层级已携带登录态导出</div>
<div style="margin-top: 12px;"> <div style="margin-top: 12px;">
<button class="bb-btn" :disabled="busy" @click="exportCloud">导出为 HTML</button> <button class="bb-btn" :disabled="busy" @click="exportCloud">导出为 HTML</button>
</div> </div>
</div> </div>
<div class="bb-card bb-card--interactive"> <div class="bb-card bb-card--interactive">
<div class="bb-cardTitle">导出本地</div> <div class="bb-cardTitle">导出本地</div>
<div class="bb-cardSub">导出当前浏览器 localStorage 中的书签平铺导出</div> <div class="bb-cardSub">导出当前浏览器 localStorage 中的书签平铺导出</div>
<div style="margin-top: 12px;"> <div style="margin-top: 12px;">
<button class="bb-btn bb-btn--secondary" :disabled="busy" @click="exportLocal">导出为 HTML</button> <button class="bb-btn bb-btn--secondary" :disabled="busy" @click="exportLocal">导出为 HTML</button>
</div> </div>
</div> </div>
</div> </div>
<div v-else class="bb-empty" style="margin-top: 12px;"> <div v-else class="bb-empty" style="margin-top: 12px;">
<div style="font-weight: 900;">登录后可导出云端书签</div> <div style="font-weight: 900;">登录后可导出云端书签</div>
<div class="bb-muted" style="margin-top: 6px;">你仍可在未登录状态下导入到本机并导出本机数据</div> <div class="bb-muted" style="margin-top: 6px;">你仍可在未登录状态下导入到本机并导出本机数据</div>
</div> </div>
</section> </section>
</template> </template>
<style scoped> <style scoped>
/* page-level tweaks only */ /* page-level tweaks only */
</style> </style>

View File

@@ -1,133 +1,133 @@
<script setup> <script setup>
import { ref } from "vue"; import { ref } from "vue";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import { apiFetch, setToken } from "../lib/api"; import { apiFetch, setToken } from "../lib/api";
import { clearLocalState, mergeLocalToUser } from "../lib/localData"; import { clearLocalState, mergeLocalToUser } from "../lib/localData";
const router = useRouter(); const router = useRouter();
const mode = ref("login"); const mode = ref("login");
const email = ref(""); const email = ref("");
const password = ref(""); const password = ref("");
const error = ref(""); const error = ref("");
const loading = ref(false); const loading = ref(false);
async function submit() { async function submit() {
loading.value = true; loading.value = true;
error.value = ""; error.value = "";
try { try {
const path = mode.value === "register" ? "/auth/register" : "/auth/login"; const path = mode.value === "register" ? "/auth/register" : "/auth/login";
const res = await apiFetch(path, { const res = await apiFetch(path, {
method: "POST", method: "POST",
body: JSON.stringify({ email: email.value, password: password.value }) body: JSON.stringify({ email: email.value, password: password.value })
}); });
setToken(res.token); setToken(res.token);
// Bootstrap: push local data to server (server enforces userId from token) // Bootstrap: push local data to server (server enforces userId from token)
const payload = mergeLocalToUser(res.user.id); const payload = mergeLocalToUser(res.user.id);
await apiFetch("/sync/push", { await apiFetch("/sync/push", {
method: "POST", method: "POST",
body: JSON.stringify(payload) body: JSON.stringify(payload)
}); });
clearLocalState(); clearLocalState();
const next = String(router.currentRoute.value.query?.next || "").trim(); const next = String(router.currentRoute.value.query?.next || "").trim();
await router.push(next || "/my"); await router.push(next || "/my");
} catch (e) { } catch (e) {
error.value = e.message || String(e); error.value = e.message || String(e);
} finally { } finally {
loading.value = false; loading.value = false;
} }
} }
</script> </script>
<template> <template>
<section class="bb-page bb-auth"> <section class="bb-page bb-auth">
<div class="bb-authCard bb-clayCard"> <div class="bb-authCard bb-clayCard">
<div class="bb-authHeader"> <div class="bb-authHeader">
<div class="bb-pill">云端同步 · 本地不丢</div> <div class="bb-pill">云端同步 · 本地不丢</div>
<h1 class="bb-authTitle">{{ mode === 'register' ? '注册账号' : '登录账号' }}</h1> <h1 class="bb-authTitle">{{ mode === 'register' ? '注册账号' : '登录账号' }}</h1>
<p class="bb-heroSub">登录后自动同步到云端不登录也能本地管理localStorage</p> <p class="bb-heroSub">登录后自动同步到云端不登录也能本地管理localStorage</p>
</div> </div>
<div class="bb-seg" role="tablist" aria-label="登录模式"> <div class="bb-seg" role="tablist" aria-label="登录模式">
<button <button
class="bb-segBtn" class="bb-segBtn"
:class="{ active: mode === 'login' }" :class="{ active: mode === 'login' }"
role="tab" role="tab"
:aria-selected="mode === 'login'" :aria-selected="mode === 'login'"
@click="mode = 'login'" @click="mode = 'login'"
> >
登录 登录
</button> </button>
<button <button
class="bb-segBtn" class="bb-segBtn"
:class="{ active: mode === 'register' }" :class="{ active: mode === 'register' }"
role="tab" role="tab"
:aria-selected="mode === 'register'" :aria-selected="mode === 'register'"
@click="mode = 'register'" @click="mode = 'register'"
> >
注册 注册
</button> </button>
</div> </div>
<div class="bb-authForm"> <div class="bb-authForm">
<label class="bb-field"> <label class="bb-field">
<span class="bb-label">邮箱</span> <span class="bb-label">邮箱</span>
<input v-model="email" class="bb-input" placeholder="name@example.com" autocomplete="email" /> <input v-model="email" class="bb-input" placeholder="name@example.com" autocomplete="email" />
</label> </label>
<label class="bb-field"> <label class="bb-field">
<span class="bb-label">密码</span> <span class="bb-label">密码</span>
<input <input
v-model="password" v-model="password"
class="bb-input" class="bb-input"
placeholder="至少 8 位" placeholder="至少 8 位"
type="password" type="password"
autocomplete="current-password" autocomplete="current-password"
/> />
</label> </label>
<button class="bb-btn" :disabled="loading" @click="submit"> <button class="bb-btn" :disabled="loading" @click="submit">
{{ loading ? '处理中' : (mode === 'register' ? '创建账号并登录' : '登录') }} {{ loading ? '处理中' : (mode === 'register' ? '创建账号并登录' : '登录') }}
</button> </button>
<p v-if="error" class="bb-alert bb-alert--error" style="margin: 0;">{{ error }}</p> <p v-if="error" class="bb-alert bb-alert--error" style="margin: 0;">{{ error }}</p>
</div> </div>
<div class="bb-authFoot bb-muted"> <div class="bb-authFoot bb-muted">
Token 会持久化保存仅当你手动退出才会清除 Token 会持久化保存仅当你手动退出才会清除
</div> </div>
</div> </div>
<aside class="bb-authAside bb-card"> <aside class="bb-authAside bb-card">
<div class="bb-cardTitle">你将获得</div> <div class="bb-cardTitle">你将获得</div>
<div class="bb-cardSub">更像课程平台的学习型收藏体验</div> <div class="bb-cardSub">更像课程平台的学习型收藏体验</div>
<div class="bb-authBadges"> <div class="bb-authBadges">
<span class="bb-tag">同步</span> <span class="bb-tag">同步</span>
<span class="bb-tag bb-tag2">去重</span> <span class="bb-tag bb-tag2">去重</span>
<span class="bb-tag bb-tag3">文件夹层级</span> <span class="bb-tag bb-tag3">文件夹层级</span>
</div> </div>
<ul class="bb-bullets" style="margin-top: 10px;"> <ul class="bb-bullets" style="margin-top: 10px;">
<li>导入 Chrome/Edge 书签 HTML 一键合并</li> <li>导入 Chrome/Edge 书签 HTML 一键合并</li>
<li>公开/私有可切换公开页可分享</li> <li>公开/私有可切换公开页可分享</li>
<li>跨标签页登录态自动同步</li> <li>跨标签页登录态自动同步</li>
</ul> </ul>
<div class="bb-miniQuote" style="margin-top: 12px;"> <div class="bb-miniQuote" style="margin-top: 12px;">
<div class="bb-quote">终于把书签变成了可复习的知识库</div> <div class="bb-quote">终于把书签变成了可复习的知识库</div>
<div class="bb-quoteBy"> 早八人 · 收藏课学员</div> <div class="bb-quoteBy"> 早八人 · 收藏课学员</div>
</div> </div>
</aside> </aside>
</section> </section>
</template> </template>
<style scoped> <style scoped>
.bb-authTitle { margin: 10px 0 6px; } .bb-authTitle { margin: 10px 0 6px; }
.bb-authForm { display: grid; gap: 10px; margin-top: 12px; } .bb-authForm { display: grid; gap: 10px; margin-top: 12px; }
.bb-authFoot { margin-top: 10px; } .bb-authFoot { margin-top: 10px; }
</style> </style>

File diff suppressed because it is too large Load Diff

View File

@@ -1,180 +1,180 @@
<script setup> <script setup>
import { computed, onBeforeUnmount, onMounted, ref, watch } from "vue"; import { computed, onBeforeUnmount, onMounted, ref, watch } from "vue";
import BbSelect from "../components/BbSelect.vue"; import BbSelect from "../components/BbSelect.vue";
import BbModal from "../components/BbModal.vue"; import BbModal from "../components/BbModal.vue";
import { apiFetch, tokenRef } from "../lib/api"; import { apiFetch, tokenRef } from "../lib/api";
const q = ref(""); const q = ref("");
const loading = ref(false); const loading = ref(false);
const error = ref(""); const error = ref("");
const items = ref([]); const items = ref([]);
const loggedIn = computed(() => Boolean(tokenRef.value)); const loggedIn = computed(() => Boolean(tokenRef.value));
const addTitle = ref(""); const addTitle = ref("");
const addUrl = ref(""); const addUrl = ref("");
const addVisibility = ref("public"); const addVisibility = ref("public");
const addFolderId = ref(null); const addFolderId = ref(null);
const addBusy = ref(false); const addBusy = ref(false);
const addStatus = ref(""); const addStatus = ref("");
const addModalOpen = ref(false); const addModalOpen = ref(false);
const folders = ref([]); const folders = ref([]);
const foldersLoading = ref(false); const foldersLoading = ref(false);
const folderOptions = computed(() => [ const folderOptions = computed(() => [
{ value: null, label: "(无文件夹)" }, { value: null, label: "(无文件夹)" },
...folders.value.map((f) => ({ value: f.id, label: f.name })) ...folders.value.map((f) => ({ value: f.id, label: f.name }))
]); ]);
const visibilityOptions = [ const visibilityOptions = [
{ value: "public", label: "公开" }, { value: "public", label: "公开" },
{ value: "private", label: "私有" } { value: "private", label: "私有" }
]; ];
async function loadFolders() { async function loadFolders() {
if (!loggedIn.value) return; if (!loggedIn.value) return;
foldersLoading.value = true; foldersLoading.value = true;
try { try {
folders.value = await apiFetch("/folders"); folders.value = await apiFetch("/folders");
} finally { } finally {
foldersLoading.value = false; foldersLoading.value = false;
} }
} }
async function addBookmark() { async function addBookmark() {
const title = addTitle.value.trim(); const title = addTitle.value.trim();
const url = addUrl.value.trim(); const url = addUrl.value.trim();
if (!title || !url) return; if (!title || !url) return;
addBusy.value = true; addBusy.value = true;
addStatus.value = ""; addStatus.value = "";
error.value = ""; error.value = "";
try { try {
await apiFetch("/bookmarks", { await apiFetch("/bookmarks", {
method: "POST", method: "POST",
body: JSON.stringify({ body: JSON.stringify({
folderId: addFolderId.value ?? null, folderId: addFolderId.value ?? null,
title, title,
url, url,
visibility: addVisibility.value visibility: addVisibility.value
}) })
}); });
addTitle.value = ""; addTitle.value = "";
addUrl.value = ""; addUrl.value = "";
addFolderId.value = null; addFolderId.value = null;
addVisibility.value = "public"; addVisibility.value = "public";
addStatus.value = "已添加"; addStatus.value = "已添加";
addModalOpen.value = false; addModalOpen.value = false;
await load(); await load();
} catch (e) { } catch (e) {
error.value = e.message || String(e); error.value = e.message || String(e);
} finally { } finally {
addBusy.value = false; addBusy.value = false;
} }
} }
async function load() { async function load() {
loading.value = true; loading.value = true;
error.value = ""; error.value = "";
try { try {
items.value = await apiFetch(`/bookmarks/public?q=${encodeURIComponent(q.value)}`); items.value = await apiFetch(`/bookmarks/public?q=${encodeURIComponent(q.value)}`);
} catch (e) { } catch (e) {
error.value = e.message || String(e); error.value = e.message || String(e);
} finally { } finally {
loading.value = false; loading.value = false;
} }
} }
let searchTimer = 0; let searchTimer = 0;
watch( watch(
() => q.value, () => q.value,
() => { () => {
window.clearTimeout(searchTimer); window.clearTimeout(searchTimer);
searchTimer = window.setTimeout(() => { searchTimer = window.setTimeout(() => {
load(); load();
}, 200); }, 200);
} }
); );
onBeforeUnmount(() => { onBeforeUnmount(() => {
window.clearTimeout(searchTimer); window.clearTimeout(searchTimer);
}); });
onMounted(load); onMounted(load);
onMounted(loadFolders); onMounted(loadFolders);
</script> </script>
<template> <template>
<section class="bb-page"> <section class="bb-page">
<div class="bb-pageHeader"> <div class="bb-pageHeader">
<div class="bb-row" style="justify-content: space-between; align-items: flex-end; gap: 12px; flex-wrap: wrap;"> <div class="bb-row" style="justify-content: space-between; align-items: flex-end; gap: 12px; flex-wrap: wrap;">
<div> <div>
<h1 style="margin: 0;">公开书签</h1> <h1 style="margin: 0;">公开书签</h1>
<div class="bb-muted" style="margin-top: 4px;">无需登录即可浏览本页面书签</div> <div class="bb-muted" style="margin-top: 4px;">无需登录即可浏览本页面书签</div>
</div> </div>
<div v-if="loggedIn" class="bb-row" style="gap: 10px; flex-wrap: wrap;"> <div v-if="loggedIn" class="bb-row" style="gap: 10px; flex-wrap: wrap;">
<button class="bb-btn" type="button" @click="addModalOpen = true">添加书签</button> <button class="bb-btn" type="button" @click="addModalOpen = true">添加书签</button>
</div> </div>
</div> </div>
</div> </div>
<div class="bb-card" style="margin-top: 12px;"> <div class="bb-card" style="margin-top: 12px;">
<div class="bb-row"> <div class="bb-row">
<div class="bb-searchWrap"> <div class="bb-searchWrap">
<input v-model="q" class="bb-input bb-input--withClear" placeholder="搜索标题或链接" /> <input v-model="q" class="bb-input bb-input--withClear" placeholder="搜索标题或链接" />
<button v-if="q.trim()" class="bb-searchClear" type="button" aria-label="清空搜索" @click="q = ''">×</button> <button v-if="q.trim()" class="bb-searchClear" type="button" aria-label="清空搜索" @click="q = ''">×</button>
</div> </div>
</div> </div>
<p v-if="error" class="bb-alert bb-alert--error" style="margin-top: 12px;">{{ error }}</p> <p v-if="error" class="bb-alert bb-alert--error" style="margin-top: 12px;">{{ error }}</p>
<p v-else-if="loading" class="bb-muted" style="margin-top: 12px;">加载中</p> <p v-else-if="loading" class="bb-muted" style="margin-top: 12px;">加载中</p>
<div v-else-if="!items.length" class="bb-empty" style="margin-top: 12px;"> <div v-else-if="!items.length" class="bb-empty" style="margin-top: 12px;">
<div style="font-weight: 800;">暂无公开书签</div> <div style="font-weight: 800;">暂无公开书签</div>
<div class="bb-muted" style="margin-top: 6px;">换个关键词试试或稍后再来</div> <div class="bb-muted" style="margin-top: 6px;">换个关键词试试或稍后再来</div>
</div> </div>
</div> </div>
<BbModal v-if="loggedIn" v-model="addModalOpen" title="添加书签"> <BbModal v-if="loggedIn" v-model="addModalOpen" title="添加书签">
<div class="bb-publicAdd" style="margin-top: 2px;"> <div class="bb-publicAdd" style="margin-top: 2px;">
<input v-model="addTitle" class="bb-input" placeholder="标题" /> <input v-model="addTitle" class="bb-input" placeholder="标题" />
<input v-model="addUrl" class="bb-input" placeholder="链接https://..." /> <input v-model="addUrl" class="bb-input" placeholder="链接https://..." />
<BbSelect <BbSelect
v-model="addFolderId" v-model="addFolderId"
:options="folderOptions" :options="folderOptions"
:disabled="foldersLoading" :disabled="foldersLoading"
placeholder="选择文件夹" placeholder="选择文件夹"
/> />
<BbSelect v-model="addVisibility" :options="visibilityOptions" /> <BbSelect v-model="addVisibility" :options="visibilityOptions" />
<div class="bb-row" style="justify-content: flex-end; gap: 10px;"> <div class="bb-row" style="justify-content: flex-end; gap: 10px;">
<button class="bb-btn bb-btn--secondary" type="button" @click="addModalOpen = false">取消</button> <button class="bb-btn bb-btn--secondary" type="button" @click="addModalOpen = false">取消</button>
<button class="bb-btn" type="button" :disabled="addBusy" @click="addBookmark">添加</button> <button class="bb-btn" type="button" :disabled="addBusy" @click="addBookmark">添加</button>
</div> </div>
</div> </div>
</BbModal> </BbModal>
<p v-if="addStatus" class="bb-alert bb-alert--ok" style="margin-top: 12px;">{{ addStatus }}</p> <p v-if="addStatus" class="bb-alert bb-alert--ok" style="margin-top: 12px;">{{ addStatus }}</p>
<ul v-if="!loading && !error && items.length" class="bb-publicList"> <ul v-if="!loading && !error && items.length" class="bb-publicList">
<li v-for="b in items" :key="b.id" class="bb-card bb-card--interactive bb-clickCard"> <li v-for="b in items" :key="b.id" class="bb-card bb-card--interactive bb-clickCard">
<a :href="b.url" target="_blank" rel="noopener" class="bb-cardLink"> <a :href="b.url" target="_blank" rel="noopener" class="bb-cardLink">
<div class="bb-bookmarkTitle" style="font-weight: 900; color: var(--bb-text);">{{ b.title }}</div> <div class="bb-bookmarkTitle" style="font-weight: 900; color: var(--bb-text);">{{ b.title }}</div>
<div class="bb-muted" style="overflow-wrap: anywhere; margin-top: 6px;">{{ b.url }}</div> <div class="bb-muted" style="overflow-wrap: anywhere; margin-top: 6px;">{{ b.url }}</div>
</a> </a>
</li> </li>
</ul> </ul>
</section> </section>
</template> </template>
<style scoped> <style scoped>
.bb-publicList { list-style: none; padding: 0; display: grid; grid-template-columns: 1fr; gap: 10px; margin-top: 12px; } .bb-publicList { list-style: none; padding: 0; display: grid; grid-template-columns: 1fr; gap: 10px; margin-top: 12px; }
@media (min-width: 768px) { .bb-publicList { grid-template-columns: 1fr 1fr; } } @media (min-width: 768px) { .bb-publicList { grid-template-columns: 1fr 1fr; } }
.bb-publicAdd { display: grid; gap: 10px; grid-template-columns: 1fr; } .bb-publicAdd { display: grid; gap: 10px; grid-template-columns: 1fr; }
.bb-clickCard { padding: 0; } .bb-clickCard { padding: 0; }
.bb-cardLink { display: block; padding: 12px; text-decoration: none; color: inherit; } .bb-cardLink { display: block; padding: 12px; text-decoration: none; color: inherit; }
.bb-cardLink:hover .bb-bookmarkTitle { color: var(--bb-cta); } .bb-cardLink:hover .bb-bookmarkTitle { color: var(--bb-cta); }
</style> </style>

View File

@@ -1,41 +1,41 @@
import { createRouter, createWebHistory } from "vue-router"; import { createRouter, createWebHistory } from "vue-router";
import PublicPage from "./pages/PublicPage.vue"; import PublicPage from "./pages/PublicPage.vue";
import LoginPage from "./pages/LoginPage.vue"; import LoginPage from "./pages/LoginPage.vue";
import MyPage from "./pages/MyPage.vue"; import MyPage from "./pages/MyPage.vue";
import ImportExportPage from "./pages/ImportExportPage.vue"; import ImportExportPage from "./pages/ImportExportPage.vue";
import AdminPage from "./pages/AdminPage.vue"; import AdminPage from "./pages/AdminPage.vue";
import { ensureMe, tokenRef } from "./lib/api"; import { ensureMe, tokenRef } from "./lib/api";
export const router = createRouter({ export const router = createRouter({
history: createWebHistory(), history: createWebHistory(),
routes: [ routes: [
{ path: "/", component: PublicPage }, { path: "/", component: PublicPage },
{ path: "/login", component: LoginPage }, { path: "/login", component: LoginPage },
{ path: "/my", component: MyPage }, { path: "/my", component: MyPage },
{ path: "/import", component: ImportExportPage }, { path: "/import", component: ImportExportPage },
{ path: "/admin", component: AdminPage } { path: "/admin", component: AdminPage }
] ]
}); });
router.beforeEach(async (to) => { router.beforeEach(async (to) => {
const loggedIn = Boolean(tokenRef.value); const loggedIn = Boolean(tokenRef.value);
// 主页(/)永远是公共首页;不因登录态自动跳转 // 主页(/)永远是公共首页;不因登录态自动跳转
// 已登录访问登录页:直接去“我的” // 已登录访问登录页:直接去“我的”
if (to.path === "/login" && loggedIn) return { path: "/my" }; if (to.path === "/login" && loggedIn) return { path: "/my" };
// 导入导出:登录后才可见/可用 // 导入导出:登录后才可见/可用
if (to.path === "/import" && !loggedIn) { if (to.path === "/import" && !loggedIn) {
return { path: "/login", query: { next: to.fullPath } }; return { path: "/login", query: { next: to.fullPath } };
} }
// 管理界面:仅管理员可见 // 管理界面:仅管理员可见
if (to.path.startsWith("/admin")) { if (to.path.startsWith("/admin")) {
if (!loggedIn) return { path: "/login", query: { next: to.fullPath } }; if (!loggedIn) return { path: "/login", query: { next: to.fullPath } };
const me = await ensureMe(); const me = await ensureMe();
if (!me || me.role !== "admin") return { path: "/my" }; if (!me || me.role !== "admin") return { path: "/my" };
} }
return true; return true;
}); });

View File

@@ -0,0 +1,13 @@
server {
listen 80;
server_name mark.cloud-xl.top;
location / {
proxy_pass http://host.docker.internal:3001;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}

View File

@@ -1,14 +1,14 @@
server { server {
listen 6666; listen 80;
server_name mark.cloud-xl.top; server_name mark.cloud-xl.top;
root /opt/browser-bookmark/web; root /usr/share/nginx/html;
index index.html; index index.html;
location / { location / {
try_files $uri $uri/ /index.html; try_files $uri $uri/ /index.html;
} }
access_log /var/log/nginx/mark.cloud-xl.top.access.log; access_log /var/log/nginx/mark.cloud-xl.top.access.log;
error_log /var/log/nginx/mark.cloud-xl.top.error.log; error_log /var/log/nginx/mark.cloud-xl.top.error.log;
} }

View File

@@ -1,16 +1,16 @@
[Unit] [Unit]
Description=Browser Bookmark Server Description=Browser Bookmark Server
After=network.target After=network.target
[Service] [Service]
Type=simple Type=simple
WorkingDirectory=/opt/browser-bookmark/server WorkingDirectory=/home/xxl/Code/bookmark/apps/server
EnvironmentFile=/opt/browser-bookmark/server/.env EnvironmentFile=/home/xxl/Code/bookmark/.env
ExecStart=/usr/bin/node src/index.js ExecStart=/usr/bin/env node src/index.js
Restart=always Restart=always
RestartSec=5 RestartSec=5
User=root User=root
Group=root Group=root
[Install] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target

View File

@@ -1,43 +1,43 @@
<!DOCTYPE NETSCAPE-Bookmark-file-1> <!DOCTYPE NETSCAPE-Bookmark-file-1>
<!-- This is an automatically generated file. <!-- This is an automatically generated file.
It will be read and overwritten. It will be read and overwritten.
DO NOT EDIT! --> DO NOT EDIT! -->
<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8"> <META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8">
<TITLE>Bookmarks</TITLE> <TITLE>Bookmarks</TITLE>
<H1>Bookmarks</H1> <H1>Bookmarks</H1>
<DL><p> <DL><p>
<DT><H3 ADD_DATE="1723650863" LAST_MODIFIED="1768653657" PERSONAL_TOOLBAR_FOLDER="true">书签栏</H3> <DT><H3 ADD_DATE="1723650863" LAST_MODIFIED="1768653657" PERSONAL_TOOLBAR_FOLDER="true">书签栏</H3>
<DL><p> <DL><p>
<DT><H3 ADD_DATE="1723650865" LAST_MODIFIED="1768653657"> 工 作 </H3> <DT><H3 ADD_DATE="1723650865" LAST_MODIFIED="1768653657"> 工 作 </H3>
<DL><p> <DL><p>
<DT><A HREF="http://10.8.8.109:5666/" ADD_DATE="1768199010">飞牛 fnOS 109</A> <DT><A HREF="http://10.8.8.109:5666/" ADD_DATE="1768199010">飞牛 fnOS 109</A>
<DT><A HREF="http://10.8.8.109:8081/browser/" ADD_DATE="1767148578">109 pgAdmin 4</A> <DT><A HREF="http://10.8.8.109:8081/browser/" ADD_DATE="1767148578">109 pgAdmin 4</A>
<DT><A HREF="https://wx-xcx-check.blv-oa.com:5005/login" ADD_DATE="1766625532">语音小程序后台</A> <DT><A HREF="https://wx-xcx-check.blv-oa.com:5005/login" ADD_DATE="1766625532">语音小程序后台</A>
<DT><A HREF="https://www.esinidc.com/fuwuqituoguan/###" ADD_DATE="1759827003">深圳服务器托管-深圳数据中心-特价服务器托管-主机托管-易信科技-20年IDC服务商-绿色智算中心服务商-</A> <DT><A HREF="https://www.esinidc.com/fuwuqituoguan/###" ADD_DATE="1759827003">深圳服务器托管-深圳数据中心-特价服务器托管-主机托管-易信科技-20年IDC服务商-绿色智算中心服务商-</A>
<DT><A HREF="https://github.com/QPromise/EasyTrans" ADD_DATE="1698725985">GitHub - QPromise/EasyTrans: 支持谷歌翻译、百度翻译、有道翻译的免费接口基于Django、PyMuPDF实现了pdf文档英译汉的功能翻译后的pdf格式基本保持不变可以下载docx和pdf格式的翻译文档基本解决复制caj中文论文时的格式问题简单的满足看论文以及写总结的需求。</A> <DT><A HREF="https://github.com/QPromise/EasyTrans" ADD_DATE="1698725985">GitHub - QPromise/EasyTrans: 支持谷歌翻译、百度翻译、有道翻译的免费接口基于Django、PyMuPDF实现了pdf文档英译汉的功能翻译后的pdf格式基本保持不变可以下载docx和pdf格式的翻译文档基本解决复制caj中文论文时的格式问题简单的满足看论文以及写总结的需求。</A>
</DL><p> </DL><p>
<DT><H3 ADD_DATE="1723650865" LAST_MODIFIED="1768653657"> 学 习 </H3> <DT><H3 ADD_DATE="1723650865" LAST_MODIFIED="1768653657"> 学 习 </H3>
<DL><p> <DL><p>
<DT><A HREF="https://doc.dcloud.net.cn/uni-app-x/" ADD_DATE="1768478968">uni-app x 是什么? | uni-app x</A> <DT><A HREF="https://doc.dcloud.net.cn/uni-app-x/" ADD_DATE="1768478968">uni-app x 是什么? | uni-app x</A>
<DT><A HREF="https://vuejs-core.cn/admin-plus/#/other/cssfx" ADD_DATE="1766815913" ICON="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAACrElEQVQ4jW1TMYhcVRQ95773/szu6qyG2V0LkRAbiyBBCUZJEUTixtUtFCIo2AqCbRpJGAISjIWQWFiphbHQ0rhiJTErJiSLlYhFFAQ1GFZ340x25//37rGYnTERT3l597xzzr2XAHDoy96zDOms17kBRYoQBAKQ6KC2GcJvgK/+vXnr3bWjb21iBxEAXOhUVdyNUoAQYJUBACQAEOAAnA9bqhY7wvOPf/bG8rfPvfkrAMadh67sDsJ9Y8j6ly3SzAiAMYB3B3AhFeVc0uzMI76htwG8BO0oMJAQjNFUrg85PH/dvOg06nJJLSxYiq9VD83ubR2ZUx4MC2mLj330eucyz96MYy8gIEgwglMRmi4r33167gIAPHr45a3yw+DDsq/j9mCKaJT6VYyTDLBjlSRRRO83wHpzYO+e5UHspHk0esX2tGXdCqGqrGzlK98ffeevSQYTBUWwboXWk13AeGoacJiFcE8FPtD2MBVjHtQ/03Vs9OV/CbIjzLUQd8+MUiEksFF2YFCCZ7cCP3NxqXd11AHZHRaMLBs1h5fW0VzZtHy1H32tn/zHQfK6kIGeUjp96Ivee+iBdyoQgGTSjcbrlT+iy1fQaBWmKVW2mHa19ldPzyvePx3S7F2vHtx/4vIqTn5gt1sgJAV4aCWgZZ+vXTh3au2rj0/0q/JM+X17vfnmT8KVlb1ECy+MVuA2aMwEQBLH9Vvt+waCJAckmLyYgM7/jpHFQ9lugJIP7zv4YrRgLW7ceCrMtbvVE7ucgc4QAzC89u8tQALhyi52K6uOzLmFsEwLyySBNoH56DYTCzOiW0Mke39CQMIYzdDAwr0tpIVpCBpf0wgZRjMTHXmrPv71Uu/iZApecLPU+Se5GpVMH5ZxGDIiS2hkGFi0a01dPlld6p2HQBD6B7R9S6vcTSzkAAAAAElFTkSuQmCC">Vue模版 Admin Plus</A> <DT><A HREF="https://vuejs-core.cn/admin-plus/#/other/cssfx" ADD_DATE="1766815913" ICON="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAACrElEQVQ4jW1TMYhcVRQ95773/szu6qyG2V0LkRAbiyBBCUZJEUTixtUtFCIo2AqCbRpJGAISjIWQWFiphbHQ0rhiJTErJiSLlYhFFAQ1GFZ340x25//37rGYnTERT3l597xzzr2XAHDoy96zDOms17kBRYoQBAKQ6KC2GcJvgK/+vXnr3bWjb21iBxEAXOhUVdyNUoAQYJUBACQAEOAAnA9bqhY7wvOPf/bG8rfPvfkrAMadh67sDsJ9Y8j6ly3SzAiAMYB3B3AhFeVc0uzMI76htwG8BO0oMJAQjNFUrg85PH/dvOg06nJJLSxYiq9VD83ubR2ZUx4MC2mLj330eucyz96MYy8gIEgwglMRmi4r33167gIAPHr45a3yw+DDsq/j9mCKaJT6VYyTDLBjlSRRRO83wHpzYO+e5UHspHk0esX2tGXdCqGqrGzlK98ffeevSQYTBUWwboXWk13AeGoacJiFcE8FPtD2MBVjHtQ/03Vs9OV/CbIjzLUQd8+MUiEksFF2YFCCZ7cCP3NxqXd11AHZHRaMLBs1h5fW0VzZtHy1H32tn/zHQfK6kIGeUjp96Ivee+iBdyoQgGTSjcbrlT+iy1fQaBWmKVW2mHa19ldPzyvePx3S7F2vHtx/4vIqTn5gt1sgJAV4aCWgZZ+vXTh3au2rj0/0q/JM+X17vfnmT8KVlb1ECy+MVuA2aMwEQBLH9Vvt+waCJAckmLyYgM7/jpHFQ9lugJIP7zv4YrRgLW7ceCrMtbvVE7ucgc4QAzC89u8tQALhyi52K6uOzLmFsEwLyySBNoH56DYTCzOiW0Mke39CQMIYzdDAwr0tpIVpCBpf0wgZRjMTHXmrPv71Uu/iZApecLPU+Se5GpVMH5ZxGDIiS2hkGFi0a01dPlld6p2HQBD6B7R9S6vcTSzkAAAAAElFTkSuQmCC">Vue模版 Admin Plus</A>
<DT><A HREF="https://hellogithub.com/" ADD_DATE="1762827790" ICON="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABl0lEQVQ4jX2TzUvVQRSGn3O9RWFgWpEElxTsL4hAIgihhdlC7UOkRUi6CIX+iRa1ryBo0bLaB0Jx4+6lfbsgL35ESNaijfq46Pxkul4cGM6cM+95Z+a8c6AYaq3DP6OePQoT5UZE7KnngcfAGDAA1IFfQBN4HhHtCnuIVZ1Q22pTnVNHcz5UV9Tf6u3/bqJG2lsJuE+Xob5TP6jf1ZkyF/WCuqXOdiTV1Zp6LfdPFgeNlMCnajPXPeqQerG43Vv1TYF/r948eIL6RZ0vEl4naU0dVjfVK0leV8+pxyuC0+pX9WrxnG/qaPrP1I/d6kJKVEnZk/YB0ACm1UlgFnipPgH6gN2cbeBVLTX+C1wqCJaBU8AksAGsAMNAf8bHgEVgpx4Rqi1gXN0CTgBTEbGb8eWIaAGtUlJgLSJ2qkAj6/BDfZSx6+p6FuxYVTR1Rt1WGwcqpJ1Q99T59HuzBqXcd9U/6p0yt/zK4+qq+jllvZxfeUH9pP5U75U53ZppEFgCbgBVJ27zr5leRMTqoWYqSTr8fnXgKMw+XL2qLL6m4dsAAAAASUVORK5CYII=">有趣的开源社区 - HelloGitHub</A> <DT><A HREF="https://hellogithub.com/" ADD_DATE="1762827790" ICON="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABl0lEQVQ4jX2TzUvVQRSGn3O9RWFgWpEElxTsL4hAIgihhdlC7UOkRUi6CIX+iRa1ryBo0bLaB0Jx4+6lfbsgL35ESNaijfq46Pxkul4cGM6cM+95Z+a8c6AYaq3DP6OePQoT5UZE7KnngcfAGDAA1IFfQBN4HhHtCnuIVZ1Q22pTnVNHcz5UV9Tf6u3/bqJG2lsJuE+Xob5TP6jf1ZkyF/WCuqXOdiTV1Zp6LfdPFgeNlMCnajPXPeqQerG43Vv1TYF/r948eIL6RZ0vEl4naU0dVjfVK0leV8+pxyuC0+pX9WrxnG/qaPrP1I/d6kJKVEnZk/YB0ACm1UlgFnipPgH6gN2cbeBVLTX+C1wqCJaBU8AksAGsAMNAf8bHgEVgpx4Rqi1gXN0CTgBTEbGb8eWIaAGtUlJgLSJ2qkAj6/BDfZSx6+p6FuxYVTR1Rt1WGwcqpJ1Q99T59HuzBqXcd9U/6p0yt/zK4+qq+jllvZxfeUH9pP5U75U53ZppEFgCbgBVJ27zr5leRMTqoWYqSTr8fnXgKMw+XL2qLL6m4dsAAAAASUVORK5CYII=">有趣的开源社区 - HelloGitHub</A>
</DL><p> </DL><p>
<DT><H3 ADD_DATE="1740830720" LAST_MODIFIED="1768653657"> NAS </H3> <DT><H3 ADD_DATE="1740830720" LAST_MODIFIED="1768653657"> NAS </H3>
<DL><p> <DL><p>
<DT><A HREF="https://blog.csdn.net/qq_36011182/article/details/138074242" ADD_DATE="1756695839">绿联搭建rustdesk服务器_rustdesk-server-s6-CSDN博客</A> <DT><A HREF="https://blog.csdn.net/qq_36011182/article/details/138074242" ADD_DATE="1756695839">绿联搭建rustdesk服务器_rustdesk-server-s6-CSDN博客</A>
<DT><A HREF="https://www.dytt8899.com/" ADD_DATE="1742032232" ICON="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAADLklEQVQ4jW2TS2hcZRiGn/8//5nMmWsy6WTSpkntaJvQRnpJL4KrLKKxSNVKA6LFjSBFIbgQq1KCrVRQUNGqiFBqFxaNIErFqguh9VZjMZIiEpsmDUlNyNS5nplzmTm/i9LSRp/V+y2ed/W9AmD/2Uy2K75jOKys5roMLWqvej5Tnfr+1NyLC2d6dq7pTrQ37U6FlvriFPqFqHMTYt/XmeiONQ/8vDbR1Rt2vqK1rjA9iWnPBdi5Tz8P3Xv01ciIDKye/oFEPTKYkJc8v3jyyVSqCCCGzj34yJ3pno8c9yy9TFLzNJOlyvmwG5rYbOt1fbXK6stm9sSz6UOnfooPHWyO6YH7EuKvVLH6xDvrk+fEw789czwedR+PNC6xVc4yWy2U825+w9t31eYATh5LdW6dr+xXEcM4sOWVY6PRpz8kWd/ZGzMK27UeFLsnjow1muxtseAqG+UU/5Snf3+z7+JmlvHHC7FetU6rRzs/Lo2Z94zRFqTSAePKUZmVjvQo6jQxTBqy7sDF5T4bjlQuXEv3w2e1L6Xy9y0Z6nZly1blSkWuEUbrNJZIRPd+ssIYHRpt/KflOuVyOggrqAW+KgfxgpbpTI44jujArK3usKyNLTCa+1/5rb9fElIP6lwABd9QpSAxZRnt3VWieLqOqLW0cLlzC/DtLeL7V1bIEm/okHxMawNmfeIRPaNsLzJuGsldJgLXVwhpgBcM3yg4sRCloPeKIgeDpMoSNmDGBUPTmzHfE8mxyYHWVdlvSnZArqzBAyZKkPd/ICSu4NOHZWRpMaEawLwLYUnS0hce2tS+XYVDiR99z5/pCJm35bwGuAIyEajbdyOAVgWBgHkPSnWISIQMGinHeep4v3Dk4qZ2W1aco12WoMsUYGsIDEhaIE1YrMNU7ZpsCaShabarz08fyJ4BkADDzZV3Ddf7dc8qRZuhoeDDVR9yHhR9CAKUJbCaNLFS5XD+0B2v3RjT9XD4z4W14yp6eqUZXT8+6TA9U6Oa99GBxggLdMPNmxXnucWR7g9uWePNx67vfmmfr8dGEpH0nsqSaMstuFTLzpxbqnxhaP/14svbppa/xb+MRVq/V1SwbAAAAABJRU5ErkJggg==">电影天堂_电影下载_高清首发</A> <DT><A HREF="https://www.dytt8899.com/" ADD_DATE="1742032232" ICON="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAADLklEQVQ4jW2TS2hcZRiGn/8//5nMmWsy6WTSpkntaJvQRnpJL4KrLKKxSNVKA6LFjSBFIbgQq1KCrVRQUNGqiFBqFxaNIErFqguh9VZjMZIiEpsmDUlNyNS5nplzmTm/i9LSRp/V+y2ed/W9AmD/2Uy2K75jOKys5roMLWqvej5Tnfr+1NyLC2d6dq7pTrQ37U6FlvriFPqFqHMTYt/XmeiONQ/8vDbR1Rt2vqK1rjA9iWnPBdi5Tz8P3Xv01ciIDKye/oFEPTKYkJc8v3jyyVSqCCCGzj34yJ3pno8c9yy9TFLzNJOlyvmwG5rYbOt1fbXK6stm9sSz6UOnfooPHWyO6YH7EuKvVLH6xDvrk+fEw789czwedR+PNC6xVc4yWy2U825+w9t31eYATh5LdW6dr+xXEcM4sOWVY6PRpz8kWd/ZGzMK27UeFLsnjow1muxtseAqG+UU/5Snf3+z7+JmlvHHC7FetU6rRzs/Lo2Z94zRFqTSAePKUZmVjvQo6jQxTBqy7sDF5T4bjlQuXEv3w2e1L6Xy9y0Z6nZly1blSkWuEUbrNJZIRPd+ssIYHRpt/KflOuVyOggrqAW+KgfxgpbpTI44jujArK3usKyNLTCa+1/5rb9fElIP6lwABd9QpSAxZRnt3VWieLqOqLW0cLlzC/DtLeL7V1bIEm/okHxMawNmfeIRPaNsLzJuGsldJgLXVwhpgBcM3yg4sRCloPeKIgeDpMoSNmDGBUPTmzHfE8mxyYHWVdlvSnZArqzBAyZKkPd/ICSu4NOHZWRpMaEawLwLYUnS0hce2tS+XYVDiR99z5/pCJm35bwGuAIyEajbdyOAVgWBgHkPSnWISIQMGinHeep4v3Dk4qZ2W1aco12WoMsUYGsIDEhaIE1YrMNU7ZpsCaShabarz08fyJ4BkADDzZV3Ddf7dc8qRZuhoeDDVR9yHhR9CAKUJbCaNLFS5XD+0B2v3RjT9XD4z4W14yp6eqUZXT8+6TA9U6Oa99GBxggLdMPNmxXnucWR7g9uWePNx67vfmmfr8dGEpH0nsqSaMstuFTLzpxbqnxhaP/14svbppa/xb+MRVq/V1SwbAAAAABJRU5ErkJggg==">电影天堂_电影下载_高清首发</A>
<DT><A HREF="https://post.smzdm.com/p/a0qlpgm8/" ADD_DATE="1740887633" ICON="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAADJUlEQVQ4jVWTQWhcVRSG/3Pufe8lk3SSTtUmmVQXaZXYgGOibaKILSoKCioaXCi0AXddNwXpRugmdeW2XWRcKYpCN0IRKrRU22BD0kaDbYaCpkksWDKTzMx77957jhsj9Vt//7/7CP+iABGg6eTE/kj0OAkOifcVFYUyL6jQXOp9tefi/MqOCwCEhwjvj0+T6CmytoTcw0cdUDaImtsACM6FB14wU/jhl7M7G1aAASC8N17lJJmRpFByqXNZKw06Mqr23LeaFveE1mbDBXCpk2imcaRSVYAUYPMpoG7yxZNGw8mw94mcP5slub1s5dY8AYB59yNQTy/ZDz82YiNpz/3sdnV1j9X7H212/rlx1abvjA2x6mlpZ6JdRYPdexhxonzsBPHYBNQ7SO4hF75Wd/UKa0fBNNNcoHp6c2zkO45MNMXGFH2rLRgaZl35Xf2NayQb63DfXwBEIPc3wAcrxP2Dqu2UnUC6gKIYTLF4PQwXVMkQTbxM4dYNkrVVyN0a7Gtvwl+7Ako6Eb89CRoeodBOSUHkgqoKDrOCKr7eAB0cJX66AjowDO19BLrdBD81At3agvvpMvxvS0CcAF4UQSn1AhE8Y0MI4CiB+WAK7vznal56haKpE9BWC+HXRVBPL9BdRFj9Azz0JIQMGAxSQIOA0U4X4r4y3KWL6r6qkipBm02Y58ahWQZ6rA8odIMHBmHKgwjbTfJra2pzB1IsWtjkOv5af9XfvqNIOpXyDFKvE2cZZP0eqLcE2tsPvzAP6i7qrulPQANlpfPnqLVSu87k0tlWHhq2UGDNnYgIcW9JKY4BMLRRhykPwgwdUC6XyY49LyZOuBnFjbx2d9Z2XFqqNY6OnilE5mzebOUIntA3wEgzjd94C+7yj3BLS8gXFylsbIg0tsLuNIsz4MzjQI0UYAKkfuTZanG7dax99HXowD6XffMlq43Z31sFBIKOTiEyUY+1+Fvpi/6bN6dARP+LafOFyjRn2amEbSn3AZko1FjECpigSIM8cKIz/cvL/8X08AEB0PsTo/uty4+r0iEJWtEgUNUFUsxlaV7dV6ut7LgA8A9TRbIkiB/PvgAAAABJRU5ErkJggg==">NAS双2.5G网口怎么玩网口桥接直连电脑网速叠加变5G网络绿联DX4600Pro实操_NAS存储_什么值得买</A> <DT><A HREF="https://post.smzdm.com/p/a0qlpgm8/" ADD_DATE="1740887633" ICON="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAADJUlEQVQ4jVWTQWhcVRSG/3Pufe8lk3SSTtUmmVQXaZXYgGOibaKILSoKCioaXCi0AXddNwXpRugmdeW2XWRcKYpCN0IRKrRU22BD0kaDbYaCpkksWDKTzMx77957jhsj9Vt//7/7CP+iABGg6eTE/kj0OAkOifcVFYUyL6jQXOp9tefi/MqOCwCEhwjvj0+T6CmytoTcw0cdUDaImtsACM6FB14wU/jhl7M7G1aAASC8N17lJJmRpFByqXNZKw06Mqr23LeaFveE1mbDBXCpk2imcaRSVYAUYPMpoG7yxZNGw8mw94mcP5slub1s5dY8AYB59yNQTy/ZDz82YiNpz/3sdnV1j9X7H212/rlx1abvjA2x6mlpZ6JdRYPdexhxonzsBPHYBNQ7SO4hF75Wd/UKa0fBNNNcoHp6c2zkO45MNMXGFH2rLRgaZl35Xf2NayQb63DfXwBEIPc3wAcrxP2Dqu2UnUC6gKIYTLF4PQwXVMkQTbxM4dYNkrVVyN0a7Gtvwl+7Ako6Eb89CRoeodBOSUHkgqoKDrOCKr7eAB0cJX66AjowDO19BLrdBD81At3agvvpMvxvS0CcAF4UQSn1AhE8Y0MI4CiB+WAK7vznal56haKpE9BWC+HXRVBPL9BdRFj9Azz0JIQMGAxSQIOA0U4X4r4y3KWL6r6qkipBm02Y58ahWQZ6rA8odIMHBmHKgwjbTfJra2pzB1IsWtjkOv5af9XfvqNIOpXyDFKvE2cZZP0eqLcE2tsPvzAP6i7qrulPQANlpfPnqLVSu87k0tlWHhq2UGDNnYgIcW9JKY4BMLRRhykPwgwdUC6XyY49LyZOuBnFjbx2d9Z2XFqqNY6OnilE5mzebOUIntA3wEgzjd94C+7yj3BLS8gXFylsbIg0tsLuNIsz4MzjQI0UYAKkfuTZanG7dax99HXowD6XffMlq43Z31sFBIKOTiEyUY+1+Fvpi/6bN6dARP+LafOFyjRn2amEbSn3AZko1FjECpigSIM8cKIz/cvL/8X08AEB0PsTo/uty4+r0iEJWtEgUNUFUsxlaV7dV6ut7LgA8A9TRbIkiB/PvgAAAABJRU5ErkJggg==">NAS双2.5G网口怎么玩网口桥接直连电脑网速叠加变5G网络绿联DX4600Pro实操_NAS存储_什么值得买</A>
<DT><A HREF="https://nas.cloud-xl.top:9443/desktop/#/login/account" ADD_DATE="1740839792" ICON="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAADAklEQVQ4jVWTTWtbVxCG3zm+iiwLSUYpBFsSLrUtxziOnIVX3ZaQfpB+/YnQRaClLQQCIRDIrquQrELpD4jBbgLBYJvWuxLkxkE0xG1So6uElEj3XimSrHvOzHQhB+PZzDAw7wzM+xAAPH5cu7C+vn7Vb9TPqWgKh0EAgQAFFDrsGTPSL5QKO+c/On+jUll46FWrux/fvnPr3tbmRioMQyUiKEAEQA8LVRARAEBVNTWeG//w771nK9Xq7tfe6q+r17c2N1LFYsl++cVXHhlDIqrGEKDDCw4HQUSkCn306A+7ubmRmpyYvG5e+v6Cc04vXfrGmyxNUf8g1hMnRkmUiIwHIg+iRInEKB0cxFosTdHly9967Jz6fmPBi60dSyaT6PX6+svPd3Vubo62f9/C1NT7CMMAqkA+n8eLf1/g9Nxp+m17W3/4/kckk0mK48GYJ8wqqhRFEc3Pz+Pixc+xU63ig+kZhGEA5yxOnZrA06d/YXl5GWv3H9A7YRFRj1kAUTCzDuKYWq0Ab7tdRFGITqcD5xxGR0P0el0EQYB4MAAzq6oQs8CIMCkUcRwTM6NcLoPZoVAoIZPJIJPJoFgswTmH2dkyWBiDQUyAQoTJYxGIKKyzYGY0m03EsUUUheh2u2BmBEGIOLZoNltwjmFtDBFARGCERQGFjZ0aIogMn656ZCcRgTGHmQjWWYUKmFk9EYaIgIWpXq9jdXUFz5//g1bzDdrtNhSKXG4cjUYdvV4P+/v7WDx7lkQUzAKPWYiZkU6ndXqmDL/RoGw2j6DdhiEDAAiiCNncSfh+A9Mzs5oeS8OxI1Ehjwg9a23qbaeDTz/5jBw7hQPpSYtj0UqARqAJL0F7e8/UWqsE9L1MNlczxiyvra3YiYmCJ8JEIMXIO56GVgYDCiVjRvTVq5fOGJPIZHM1b6lSufbf69f3arXdVKPhKxEpAIIe23+MiVRqLHHmTKW/VKlcIwC4efOnC0+e/Hm11XpzTvUIZ2BIoeqRHBH18/n3dhYXl25cufLdw/8BAprLhr6jgOkAAAAASUVORK5CYII=">绿联云远程</A> <DT><A HREF="https://nas.cloud-xl.top:9443/desktop/#/login/account" ADD_DATE="1740839792" ICON="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAADAklEQVQ4jVWTTWtbVxCG3zm+iiwLSUYpBFsSLrUtxziOnIVX3ZaQfpB+/YnQRaClLQQCIRDIrquQrELpD4jBbgLBYJvWuxLkxkE0xG1So6uElEj3XimSrHvOzHQhB+PZzDAw7wzM+xAAPH5cu7C+vn7Vb9TPqWgKh0EAgQAFFDrsGTPSL5QKO+c/On+jUll46FWrux/fvnPr3tbmRioMQyUiKEAEQA8LVRARAEBVNTWeG//w771nK9Xq7tfe6q+r17c2N1LFYsl++cVXHhlDIqrGEKDDCw4HQUSkCn306A+7ubmRmpyYvG5e+v6Cc04vXfrGmyxNUf8g1hMnRkmUiIwHIg+iRInEKB0cxFosTdHly9967Jz6fmPBi60dSyaT6PX6+svPd3Vubo62f9/C1NT7CMMAqkA+n8eLf1/g9Nxp+m17W3/4/kckk0mK48GYJ8wqqhRFEc3Pz+Pixc+xU63ig+kZhGEA5yxOnZrA06d/YXl5GWv3H9A7YRFRj1kAUTCzDuKYWq0Ab7tdRFGITqcD5xxGR0P0el0EQYB4MAAzq6oQs8CIMCkUcRwTM6NcLoPZoVAoIZPJIJPJoFgswTmH2dkyWBiDQUyAQoTJYxGIKKyzYGY0m03EsUUUheh2u2BmBEGIOLZoNltwjmFtDBFARGCERQGFjZ0aIogMn656ZCcRgTGHmQjWWYUKmFk9EYaIgIWpXq9jdXUFz5//g1bzDdrtNhSKXG4cjUYdvV4P+/v7WDx7lkQUzAKPWYiZkU6ndXqmDL/RoGw2j6DdhiEDAAiiCNncSfh+A9Mzs5oeS8OxI1Ehjwg9a23qbaeDTz/5jBw7hQPpSYtj0UqARqAJL0F7e8/UWqsE9L1MNlczxiyvra3YiYmCJ8JEIMXIO56GVgYDCiVjRvTVq5fOGJPIZHM1b6lSufbf69f3arXdVKPhKxEpAIIe23+MiVRqLHHmTKW/VKlcIwC4efOnC0+e/Hm11XpzTvUIZ2BIoeqRHBH18/n3dhYXl25cufLdw/8BAprLhr6jgOkAAAAASUVORK5CYII=">绿联云远程</A>
<DT><A HREF="http://nas.cloud-xl.top:16601/#/login" ADD_DATE="1740830705" ICON="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAADF0lEQVQ4jW1TXWgcVRT+zr1zd2ZnJ7vZhG02UsEaEYqi1SQIklaqiG7RB6EpRYIKKQ1YCraoVQhlXkJ9sFSItVZ9EUWhSy1UERQkaFWEEqSBKjXWlLQUd+tukv2byezce3xoElLr93QOh/OdwznfR/gfFH6ZTdcalf76QtSDG2FLNeKL06/vuLxSJgCMdcka/Kkpazab3XM10PtKtfCuYCGyTbkVm3+iimiZb2yt37p8bNef63vEasDMVNuYH2fXOQHbup8FpbSSip1kEo67MbQ6Rhcb/MOdez7feisBMwHAsWtzg5L4Nam1UbptUnaCXNbXkjo667SD42ldPyHBc5HBSP/ek2qVwFoNFPjxtO249aipO6SMr9vJw7N9/R9gJy2unzjsT3lFf3cMjK0QEDEAZCCyliDTth2Zb5a/OvPlob/g3XvIHB7v5SU3NE3vgkolvyB/e4kBWj2e8H1fAEA3YS7HED3RMo/8/vVj6MwU4dpvCJIvSlZjKhLvmSvmp7DwaYFu+QIzgYiny+V7qkHz+ygK8498Nym66mUmzhNqGaDqgSu2pkZW6iX7ekgoeOeen2GABIj4xsmneh9+dMPVbmV/nHc9QcqNiEGxwFk4iZdiKY5CIAS3tYyTd1ihs3dFA2wBQFetOYbRgfkrpvfo3VQqeH0PbEHpj/OL2x4ayQ2N1gFAv/DuMkn1JgyYIjHI/sUE+fdFN3UQBR6TePW5cWpSR8/uWnbTXLCh71JuaLTO+/fbPDwsGYkf2cQMFiAWCgisNSGxUL+S4M1m8/aJB9N0aapraKC+aZvPAGFyMqJiURNFg0SKAEMsMU/+QGtNB0Eq+22qVp0RKj6o3342K07vfIdeOT0DAHzqVCL+ef4ZKsUvI2Y2MjZayU9w0xBr70T7yBNPSx19Rq6XZe1WoTIXoLurqLo5rjgDYsFzUc9AB+q4rNUO0PRY+zYzhRM7nlSEIyKR7EeyE1juBhbSQMUGFpyKCZPvCys9QcVdwW1uXFmH+aMDXWbp72FYqa3c8nKi0RnoZuq3qOmeSX247zz+g38Bbxpazs9kOYEAAAAASUVORK5CYII=">Lucky | 登录</A> <DT><A HREF="http://nas.cloud-xl.top:16601/#/login" ADD_DATE="1740830705" ICON="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAADF0lEQVQ4jW1TXWgcVRT+zr1zd2ZnJ7vZhG02UsEaEYqi1SQIklaqiG7RB6EpRYIKKQ1YCraoVQhlXkJ9sFSItVZ9EUWhSy1UERQkaFWEEqSBKjXWlLQUd+tukv2byezce3xoElLr93QOh/OdwznfR/gfFH6ZTdcalf76QtSDG2FLNeKL06/vuLxSJgCMdcka/Kkpazab3XM10PtKtfCuYCGyTbkVm3+iimiZb2yt37p8bNef63vEasDMVNuYH2fXOQHbup8FpbSSip1kEo67MbQ6Rhcb/MOdez7feisBMwHAsWtzg5L4Nam1UbptUnaCXNbXkjo667SD42ldPyHBc5HBSP/ek2qVwFoNFPjxtO249aipO6SMr9vJw7N9/R9gJy2unzjsT3lFf3cMjK0QEDEAZCCyliDTth2Zb5a/OvPlob/g3XvIHB7v5SU3NE3vgkolvyB/e4kBWj2e8H1fAEA3YS7HED3RMo/8/vVj6MwU4dpvCJIvSlZjKhLvmSvmp7DwaYFu+QIzgYiny+V7qkHz+ygK8498Nym66mUmzhNqGaDqgSu2pkZW6iX7ekgoeOeen2GABIj4xsmneh9+dMPVbmV/nHc9QcqNiEGxwFk4iZdiKY5CIAS3tYyTd1ihs3dFA2wBQFetOYbRgfkrpvfo3VQqeH0PbEHpj/OL2x4ayQ2N1gFAv/DuMkn1JgyYIjHI/sUE+fdFN3UQBR6TePW5cWpSR8/uWnbTXLCh71JuaLTO+/fbPDwsGYkf2cQMFiAWCgisNSGxUL+S4M1m8/aJB9N0aapraKC+aZvPAGFyMqJiURNFg0SKAEMsMU/+QGtNB0Eq+22qVp0RKj6o3342K07vfIdeOT0DAHzqVCL+ef4ZKsUvI2Y2MjZayU9w0xBr70T7yBNPSx19Rq6XZe1WoTIXoLurqLo5rjgDYsFzUc9AB+q4rNUO0PRY+zYzhRM7nlSEIyKR7EeyE1juBhbSQMUGFpyKCZPvCys9QcVdwW1uXFmH+aMDXWbp72FYqa3c8nKi0RnoZuq3qOmeSX247zz+g38Bbxpazs9kOYEAAAAASUVORK5CYII=">Lucky | 登录</A>
<DT><A HREF="https://github.com/budtmo/docker-android" ADD_DATE="1740551286" ICON="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABeklEQVQ4jY3Tv2qVQRAF8PPdP7k3itoE7SOIKFZiDAZsfBJFfBDRQjBFGkFiJ/gKerGwsRAsvaKFFhYWgo3GxEA0P4vMhTUk4sDy7c6cMztzZr9kn2EFD/Eem7Xele/yfnxLnMP9Isxst9bMfhRmriX2MMLE/9ukOL1ZktUKvMANvMIGpnhT+9e4jmeFvTcjL2ELv7BevjFOYYgBFjBfsbXCbuLiIMnNJPPV0Tb6XddtJ9luJPqKDv0kO0n6SY4kuZVSeBc/cabRpWt06mZnLJaYu5im+oPPGB06pr9Fn1aCjV6SYcWOJhm3Nx9A7pKMkywk6ZIMe0k+JJHkRJLlruugf0ALg67rJFlJcrI4H4NHVc53fMLZf1RwDm/xu9peD5brcBuPa/8cSw3xAp7gW8V3apSXZoC1quI87uApTjcJFptXuFXf1VbVPl7WSK+1txdmjC9Nkom9f6fXggZ40ICON7Fj1feOvWc/zGGGK9XGqPGNcBdX9+P/AO1cPr7g2LjWAAAAAElFTkSuQmCC">Android docker</A> <DT><A HREF="https://github.com/budtmo/docker-android" ADD_DATE="1740551286" ICON="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABeklEQVQ4jY3Tv2qVQRAF8PPdP7k3itoE7SOIKFZiDAZsfBJFfBDRQjBFGkFiJ/gKerGwsRAsvaKFFhYWgo3GxEA0P4vMhTUk4sDy7c6cMztzZr9kn2EFD/Eem7Xele/yfnxLnMP9Isxst9bMfhRmriX2MMLE/9ukOL1ZktUKvMANvMIGpnhT+9e4jmeFvTcjL2ELv7BevjFOYYgBFjBfsbXCbuLiIMnNJPPV0Tb6XddtJ9luJPqKDv0kO0n6SY4kuZVSeBc/cabRpWt06mZnLJaYu5im+oPPGB06pr9Fn1aCjV6SYcWOJhm3Nx9A7pKMkywk6ZIMe0k+JJHkRJLlruugf0ALg67rJFlJcrI4H4NHVc53fMLZf1RwDm/xu9peD5brcBuPa/8cSw3xAp7gW8V3apSXZoC1quI87uApTjcJFptXuFXf1VbVPl7WSK+1txdmjC9Nkom9f6fXggZ40ICON7Fj1feOvWc/zGGGK9XGqPGNcBdX9+P/AO1cPr7g2LjWAAAAAElFTkSuQmCC">Android docker</A>
</DL><p> </DL><p>
<DT><A HREF="http://www.jinrijinjia.cn/gjjj" ADD_DATE="1747792948" ICON="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAABrklEQVQ4jW2SPWgUURDHfzO7S+5yQRsRRbhGiEgQD5EQEMkp2omNhUgQUkW01UIlRUQQ08TCKBKwEizshDR+9YKFCIJFRNAgJBxiBBPvvOz7p9jbyy46zXtvmDcf//mZaAIgAIyeqXAvPT13WTki89m/n+NC+sxSECSQoi42gKxY38udBBZnOXcSVrl4mkc3UBvvxwjk2YE51uboMKdG+bwMFa5e4NJZDh0grBEleWPmeXpDf7g+wet3fHjJ7DXcuTnPq3scHiFtYb0KiHHZCTGq45claeG5ph/qb1eNSXFQc08laXpBHBNNMe4ACiRV7lxh7Tf1vYw1eL9E8wjRLvbXebzIZgqhLGswpmZ48oLWT86c5+sK9T2kHcZGuP+Mu/NYJdPK8/EjPn2k9YtKBUtIIr6tgLPRYWgQ35Erb56PL6xKFLG+jqo0hllahhiJEAgBU19WA5ChwFCVnTUe3CaIN28ZrJHEuBeWbZ5vWhCz+gMZ+3YzcYv2Bm58+c5mui09mGhuoxEZIUUdSCABMeB0ReizY3EJjVQQYbWe1hidUKDzP/Bl8xRRLwIP2Bb2YqkDwaPx3gAAAABJRU5ErkJggg==">国际金价</A> <DT><A HREF="http://www.jinrijinjia.cn/gjjj" ADD_DATE="1747792948" ICON="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAABrklEQVQ4jW2SPWgUURDHfzO7S+5yQRsRRbhGiEgQD5EQEMkp2omNhUgQUkW01UIlRUQQ08TCKBKwEizshDR+9YKFCIJFRNAgJBxiBBPvvOz7p9jbyy46zXtvmDcf//mZaAIgAIyeqXAvPT13WTki89m/n+NC+sxSECSQoi42gKxY38udBBZnOXcSVrl4mkc3UBvvxwjk2YE51uboMKdG+bwMFa5e4NJZDh0grBEleWPmeXpDf7g+wet3fHjJ7DXcuTnPq3scHiFtYb0KiHHZCTGq45claeG5ph/qb1eNSXFQc08laXpBHBNNMe4ACiRV7lxh7Tf1vYw1eL9E8wjRLvbXebzIZgqhLGswpmZ48oLWT86c5+sK9T2kHcZGuP+Mu/NYJdPK8/EjPn2k9YtKBUtIIr6tgLPRYWgQ35Erb56PL6xKFLG+jqo0hllahhiJEAgBU19WA5ChwFCVnTUe3CaIN28ZrJHEuBeWbZ5vWhCz+gMZ+3YzcYv2Bm58+c5mui09mGhuoxEZIUUdSCABMeB0ReizY3EJjVQQYbWe1hidUKDzP/Bl8xRRLwIP2Bb2YqkDwaPx3gAAAABJRU5ErkJggg==">国际金价</A>
<DT><A HREF="https://zoom.earth/" ADD_DATE="1755046787" ICON="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAC8ElEQVQ4jVWTX0/bdRSHn/OlsZRSGSwwupUW60AZRizjz8LUC81Y4ozemF35DnwFxmTBi70D0RvdjVGBXbhkRB3bspjIpIsONt2WGWxXYJaRDRz0R2mhv48XKzM7ybk65/NcfR7j/3FIEmBmceBtoB0w4B5wWVLOePIA+ACB3bAkmdkBg1MDQ0Mnh44da0i2JwCYz+b49dLFdTObAD6VdN/M3C7EqpCexmg089nYmObk62dJ30ualrQoad739cX4uBqj0QzQU82YIVl9JNIcCAZ//2bqYuxAb4p0SXq0XmHNM/Kr8Fqr6NrvrB3HvdlZPhweXioXi32e5z1wI2CFQuH0J6Ofx2K9KdJbJcplY6scYHPTWXnD2WQ6wF8PnaaLJVpSKT4eHY15nndakhkQ7xoYvP11eiacZYfMg4BmbsLysgxnhEKQz4uXOk1v9IOr8ZWKOPfRkSPerXT6UAA4PnDiRJ0hbv5t/k9XsbVHss0NY2UZ6iMQChuLC7J/kqaAiZUIGnzv3bpb6fRxByS6kwlWK6a787L2RuzFZkOPIbQDlTXw7kOdbxQfYluPZeVtOBRPACQcQC1Q3IbOvZBsgpoN4F+orzzZfSFoCcLWMhTycNDgOdstD+RyCwu010KizpRsQPUVkWwWrWFRXxEttaKwKLw8ev0F08sBWTabA8g54MKV85ObDdtYS63RGoS2iNFgxuEO6O8y2vYY3XHTB29CT5tRlnHl/OQmcMFJWrw+k/7ux/EJ+hLO1Xgl6+9ADYbCvvFKXLzVi452Q+1OyWJh5859e5a5a+mxkREtGUA4HN4XDIZ+O/fDVKzz1RS5Ben2nW1u/Gnsjzr2NPo0NQYYOGyWuzPL++8ML5VKxT7PK6w8U+Wmpmj2zJkJ5Zd9ra5JN/6QLl2Wrs9J8xlfX341ob1N0eyzVX5WphhwarBv6OTQ0eHnOw7GAchkFvhlemp95trVs1WZlqxq5C7gKaRqa6Kqc6J6y+3qXL0/1fk/3/1lqBmpcxMAAAAASUVORK5CYII=">Zoom Earth</A> <DT><A HREF="https://zoom.earth/" ADD_DATE="1755046787" ICON="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAC8ElEQVQ4jVWTX0/bdRSHn/OlsZRSGSwwupUW60AZRizjz8LUC81Y4ozemF35DnwFxmTBi70D0RvdjVGBXbhkRB3bspjIpIsONt2WGWxXYJaRDRz0R2mhv48XKzM7ybk65/NcfR7j/3FIEmBmceBtoB0w4B5wWVLOePIA+ACB3bAkmdkBg1MDQ0Mnh44da0i2JwCYz+b49dLFdTObAD6VdN/M3C7EqpCexmg089nYmObk62dJ30ualrQoad739cX4uBqj0QzQU82YIVl9JNIcCAZ//2bqYuxAb4p0SXq0XmHNM/Kr8Fqr6NrvrB3HvdlZPhweXioXi32e5z1wI2CFQuH0J6Ofx2K9KdJbJcplY6scYHPTWXnD2WQ6wF8PnaaLJVpSKT4eHY15nndakhkQ7xoYvP11eiacZYfMg4BmbsLysgxnhEKQz4uXOk1v9IOr8ZWKOPfRkSPerXT6UAA4PnDiRJ0hbv5t/k9XsbVHss0NY2UZ6iMQChuLC7J/kqaAiZUIGnzv3bpb6fRxByS6kwlWK6a787L2RuzFZkOPIbQDlTXw7kOdbxQfYluPZeVtOBRPACQcQC1Q3IbOvZBsgpoN4F+orzzZfSFoCcLWMhTycNDgOdstD+RyCwu010KizpRsQPUVkWwWrWFRXxEttaKwKLw8ev0F08sBWTabA8g54MKV85ObDdtYS63RGoS2iNFgxuEO6O8y2vYY3XHTB29CT5tRlnHl/OQmcMFJWrw+k/7ux/EJ+hLO1Xgl6+9ADYbCvvFKXLzVi452Q+1OyWJh5859e5a5a+mxkREtGUA4HN4XDIZ+O/fDVKzz1RS5Ben2nW1u/Gnsjzr2NPo0NQYYOGyWuzPL++8ML5VKxT7PK6w8U+Wmpmj2zJkJ5Zd9ra5JN/6QLl2Wrs9J8xlfX341ob1N0eyzVX5WphhwarBv6OTQ0eHnOw7GAchkFvhlemp95trVs1WZlqxq5C7gKaRqa6Kqc6J6y+3qXL0/1fk/3/1lqBmpcxMAAAAASUVORK5CYII=">Zoom Earth</A>
<DT><A HREF="https://gitee.com/wei_fu_wan/link-up" ADD_DATE="1767487291" ICON="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABsUlEQVQ4jaWTT2sTURTFf/eRSQK23YS0hUI30qGkoOAXEEUKEURQEJcushF33XTRlQu3XbW48QP4BUSIIFjwH3QhQgNJzSYWLLYLE9uZaTp9t4uXSZM4BsS7en/OPffcd88TRmKX2SUlroAuKzIPIGgLpCpkXiywvzOIl2ShlLINDtYF+1jBjBL3wFYxz32KK0Kt2ydwyT9fg95MS0yheuszXU5IqFPcqFPQOgX9Xn6gwadtPYsiTYu43VGHLW4AiOv59KuCuXT7FnOvXo6trXHMrjeLgBW8KxklriQ9F56uAhBsfeBw7Rn2V/sPAnsc9N4MA3FFGhR2FEoAC90fiOext3yf4M07zNQEks8PK4gibOcoedRaxo1K3YHnuSq/HeDyQQPJZocVnJzwLT/XUyHzqeMaFyaXG947k6RHs+jTnFmkObPI/qMnTkEY9u8FbWVAqqClwcTctavYo+OLKpMTTD68B0C0/WUAKdWhMfp6OFa+WsvejbuEWx/7YwQujBS3O6nmOQtDDd5/1tb1O5oYrm8k+D8rG8dS6/pMl8FsCti/poEFszn4D2QU9K/f+RxAOesDyEosegAAAABJRU5ErkJggg==">link-up: ⚡️⚡️⚡️开箱即用的数据同步工具!深度集成 Apache SeaTunnel 和 Apache Dolphinscheduler核心引擎极简可视化操作 + 20种数据源兼容全面支持国产数据库生态。支持单表同步、自动建表搭配灵活任务调度、实时资源监控与完整日志查看毫秒级响应 + 99.9% 稳定运行,数据同步和任务调度全程零门槛。</A> <DT><A HREF="https://gitee.com/wei_fu_wan/link-up" ADD_DATE="1767487291" ICON="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABsUlEQVQ4jaWTT2sTURTFf/eRSQK23YS0hUI30qGkoOAXEEUKEURQEJcushF33XTRlQu3XbW48QP4BUSIIFjwH3QhQgNJzSYWLLYLE9uZaTp9t4uXSZM4BsS7en/OPffcd88TRmKX2SUlroAuKzIPIGgLpCpkXiywvzOIl2ShlLINDtYF+1jBjBL3wFYxz32KK0Kt2ydwyT9fg95MS0yheuszXU5IqFPcqFPQOgX9Xn6gwadtPYsiTYu43VGHLW4AiOv59KuCuXT7FnOvXo6trXHMrjeLgBW8KxklriQ9F56uAhBsfeBw7Rn2V/sPAnsc9N4MA3FFGhR2FEoAC90fiOext3yf4M07zNQEks8PK4gibOcoedRaxo1K3YHnuSq/HeDyQQPJZocVnJzwLT/XUyHzqeMaFyaXG947k6RHs+jTnFmkObPI/qMnTkEY9u8FbWVAqqClwcTctavYo+OLKpMTTD68B0C0/WUAKdWhMfp6OFa+WsvejbuEWx/7YwQujBS3O6nmOQtDDd5/1tb1O5oYrm8k+D8rG8dS6/pMl8FsCti/poEFszn4D2QU9K/f+RxAOesDyEosegAAAABJRU5ErkJggg==">link-up: ⚡️⚡️⚡️开箱即用的数据同步工具!深度集成 Apache SeaTunnel 和 Apache Dolphinscheduler核心引擎极简可视化操作 + 20种数据源兼容全面支持国产数据库生态。支持单表同步、自动建表搭配灵活任务调度、实时资源监控与完整日志查看毫秒级响应 + 99.9% 稳定运行,数据同步和任务调度全程零门槛。</A>
</DL><p> </DL><p>
<DT><H3 ADD_DATE="1723650865" LAST_MODIFIED="1698725968">移动设备书签</H3> <DT><H3 ADD_DATE="1723650865" LAST_MODIFIED="1698725968">移动设备书签</H3>
<DT><A HREF="https://www.gamer.cn/yqqsqz/indexm" ADD_DATE="1701157371">元气骑士前传-磁场</A> <DT><A HREF="https://www.gamer.cn/yqqsqz/indexm" ADD_DATE="1701157371">元气骑士前传-磁场</A>
<DT><A HREF="http://fanyi.baidu.com/?aldtype=16047&tpltype=sigma" ADD_DATE="1698757168">百度翻译</A> <DT><A HREF="http://fanyi.baidu.com/?aldtype=16047&tpltype=sigma" ADD_DATE="1698757168">百度翻译</A>
<DT><A HREF="https://dy.mgz6.cc/" ADD_DATE="1698757168" ICON="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABtElEQVQ4jZXTT0jUURAH8M9b13BXuhgdiiBB6hIEJtQh9mIHO9YtiCCwNugiHgKhS5cg6CAe3a0OXSMkgshDS2FEZSYdtIOEkiERGNGfXbPW1+GX7fqHxb6ned+Z78y892aC9RiInZrk0Y32v+wcSqoKroXJ+vDwz+qPGVlDonNCHV+PKApuKOszGCq1BP0xI2NUkNtUuDHRmIoeg6GSAllDWxYnZXOyhhJzIHYe2GXi4hHhyxKzn9newrP3vJhP4o91cPogbz7SkmZmkZFpMVZ1pTXJT30SOtoYHmdkmtZt3DrJwjf6H/DoHXdOcWmUr0vcP0NHm3D9qXxK8tqWq7UOfyzTO8LZQ+T2Jtyq/9cK995yNOG7U2pftQbfl3n5gdw6bypwfB+vF0B7utFbxchKrJ17u8ikeThDYTzh0pIh2b9e3NrM4T1cKdW4mxMslteEzaVQsg6ZZoZPcHuS5/ONelRKqypIufB4VujazY4sO7MUX/FkthZ5d4qfv+vvJ1pRSCbxcizgfMNaG1F0NeSTSSzrE41tWRqNKeuDJMFgqKjoQVEUGwgjiqt7gE227j/X+Q+e/o9MbZlM5wAAAABJRU5ErkJggg==">Legado·阅读</A> <DT><A HREF="https://dy.mgz6.cc/" ADD_DATE="1698757168" ICON="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABtElEQVQ4jZXTT0jUURAH8M9b13BXuhgdiiBB6hIEJtQh9mIHO9YtiCCwNugiHgKhS5cg6CAe3a0OXSMkgshDS2FEZSYdtIOEkiERGNGfXbPW1+GX7fqHxb6ned+Z78y892aC9RiInZrk0Y32v+wcSqoKroXJ+vDwz+qPGVlDonNCHV+PKApuKOszGCq1BP0xI2NUkNtUuDHRmIoeg6GSAllDWxYnZXOyhhJzIHYe2GXi4hHhyxKzn9newrP3vJhP4o91cPogbz7SkmZmkZFpMVZ1pTXJT30SOtoYHmdkmtZt3DrJwjf6H/DoHXdOcWmUr0vcP0NHm3D9qXxK8tqWq7UOfyzTO8LZQ+T2Jtyq/9cK995yNOG7U2pftQbfl3n5gdw6bypwfB+vF0B7utFbxchKrJ17u8ikeThDYTzh0pIh2b9e3NrM4T1cKdW4mxMslteEzaVQsg6ZZoZPcHuS5/ONelRKqypIufB4VujazY4sO7MUX/FkthZ5d4qfv+vvJ1pRSCbxcizgfMNaG1F0NeSTSSzrE41tWRqNKeuDJMFgqKjoQVEUGwgjiqt7gE227j/X+Q+e/o9MbZlM5wAAAABJRU5ErkJggg==">Legado·阅读</A>
<DT><A HREF="http://m.diyiziti.com/Builder/93" ADD_DATE="1698757168">制作印章篆刻字体转换器在线转换-制作印章篆刻字体在线生成器-第一字体网</A> <DT><A HREF="http://m.diyiziti.com/Builder/93" ADD_DATE="1698757168">制作印章篆刻字体转换器在线转换-制作印章篆刻字体在线生成器-第一字体网</A>
</DL><p> </DL><p>

View File

@@ -1,88 +1,88 @@
开发框架约束(供 AI 创建项目使用) 开发框架约束(供 AI 创建项目使用)
目的:本文件用于约束 AI 在创建/改造项目时的技术选型、目录结构、工程化与交付流程。除非明确得到人工指令,否则 AI 不得偏离本文件的约束。 目的:本文件用于约束 AI 在创建/改造项目时的技术选型、目录结构、工程化与交付流程。除非明确得到人工指令,否则 AI 不得偏离本文件的约束。
1. 运行环境与基础约束 1. 运行环境与基础约束
- Node.js 版本:必须使用 Node.js 22+(建议使用最新 LTS - Node.js 版本:必须使用 Node.js 22+(建议使用最新 LTS
- 主要语言JavaScript.js为主。 - 主要语言JavaScript.js为主。
- 允许在必要时引入类型检查方案(例如 JSDoc + // @ts-check但默认不将 TypeScript 作为主要语言。 - 允许在必要时引入类型检查方案(例如 JSDoc + // @ts-check但默认不将 TypeScript 作为主要语言。
- 包管理器:**强制统一使用 `npm`**。 - 包管理器:**强制统一使用 `npm`**。
- 跨平台:默认需兼容 WindowsPowerShell与类 Unix 环境。 - 跨平台:默认需兼容 WindowsPowerShell与类 Unix 环境。
2. 技术栈约束 2. 技术栈约束
2.1 前端(如需要前端) 2.1 前端(如需要前端)
- 框架:必须使用 Vue 3.x。 - 框架:必须使用 Vue 3.x。
- 生态库:仅引入与 Vue 3.x 兼容的相关库;避免引入与 Vue 2.x 绑定的历史库。 - 生态库:仅引入与 Vue 3.x 兼容的相关库;避免引入与 Vue 2.x 绑定的历史库。
- 构建工具: Vite如与既有工程冲突需说明原因并保持一致性 - 构建工具: Vite如与既有工程冲突需说明原因并保持一致性
- ui-ux-pro-max美化必需遵守 - ui-ux-pro-max美化必需遵守
Create a playful educational platform landing page with claymorphism cards, course catalog preview, progress tracking demo, student testimonials, and enrollment CTA. Use vibrant, engaging colors. Create a playful educational platform landing page with claymorphism cards, course catalog preview, progress tracking demo, student testimonials, and enrollment CTA. Use vibrant, engaging colors.
2.2 后端(如需要后端) 2.2 后端(如需要后端)
- 运行时:必须使用 Node.js。 - 运行时:必须使用 Node.js。
- 语言:后端同样以 JavaScript 为主。 - 语言:后端同样以 JavaScript 为主。
- API 风格:默认使用 HTTP JSON API如采用 GraphQL/WebSocket 等需明确说明并仍遵循 OpenSpec 约束)。 - API 风格:默认使用 HTTP JSON API如采用 GraphQL/WebSocket 等需明确说明并仍遵循 OpenSpec 约束)。
3. OpenSpec规范驱动开发流程约束 3. OpenSpec规范驱动开发流程约束
> 说明:这里的 OpenSpec 指通过全局安装 `@fission-ai/openspec` 获得的规范驱动工具链;在 API 场景下,接口契约必须使用并遵守 OpenAPI 3.1。两者不冲突OpenSpec 用于驱动/校验流程OpenAPI 3.1 是规范文件中必须满足的契约。 > 说明:这里的 OpenSpec 指通过全局安装 `@fission-ai/openspec` 获得的规范驱动工具链;在 API 场景下,接口契约必须使用并遵守 OpenAPI 3.1。两者不冲突OpenSpec 用于驱动/校验流程OpenAPI 3.1 是规范文件中必须满足的契约。
3.0 OpenSpec 工具链安装(强制) 3.0 OpenSpec 工具链安装(强制)
- 开发与 CI 环境必须确保可用的 OpenSpec 工具链: - 开发与 CI 环境必须确保可用的 OpenSpec 工具链:
- 安装命令npm install -g @fission-ai/openspec@latest - 安装命令npm install -g @fission-ai/openspec@latest
- AI 在生成项目脚本时: - AI 在生成项目脚本时:
- 必须将规范校验能力接入到 npm scripts见 3.3)。 - 必须将规范校验能力接入到 npm scripts见 3.3)。
- 不得绕过 OpenSpec 校验直接交付“未受规范约束”的 API 实现。 - 不得绕过 OpenSpec 校验直接交付“未受规范约束”的 API 实现。
3.1 必须交付的规范产物 3.1 必须交付的规范产物
- 项目必须包含一个可追溯的规范文件: - 项目必须包含一个可追溯的规范文件:
- API 项目:`spec/openapi.yaml`(或 `spec/openapi.json`),版本 OpenAPI 3.1。 - API 项目:`spec/openapi.yaml`(或 `spec/openapi.json`),版本 OpenAPI 3.1。
- 非 API 项目:仍需提供对应的“规格说明”(例如流程/数据结构/输入输出契约),放在 spec/ 目录下。 - 非 API 项目:仍需提供对应的“规格说明”(例如流程/数据结构/输入输出契约),放在 spec/ 目录下。
- 规范文件需满足: - 规范文件需满足:
- 可被校验lint/validate - 可被校验lint/validate
- 与实现一致(实现变更必须同步更新规范) - 与实现一致(实现变更必须同步更新规范)
3.2 开发顺序(强制) 3.2 开发顺序(强制)
1. 先写/更新规范spec-first在新增/修改功能前,先更新 `spec/` 下的规范。 1. 先写/更新规范spec-first在新增/修改功能前,先更新 `spec/` 下的规范。
2. 再实现:实现必须与规范一致。 2. 再实现:实现必须与规范一致。
3. 再验证CI/本地脚本必须包含规范校验步骤。 3. 再验证CI/本地脚本必须包含规范校验步骤。
4. 再文档化README 中必须说明如何查看/使用规范与如何运行校验。 4. 再文档化README 中必须说明如何查看/使用规范与如何运行校验。
3.3 规范校验与联动(强制) 3.3 规范校验与联动(强制)
- 必须提供脚本(示例命名,可按项目调整但不可缺失): - 必须提供脚本(示例命名,可按项目调整但不可缺失):
- npm run spec:lint调用 OpenSpec 对 spec/ 做 lint具体 CLI 参数以 openspec --help 为准) - npm run spec:lint调用 OpenSpec 对 spec/ 做 lint具体 CLI 参数以 openspec --help 为准)
- npm run spec:validate调用 OpenSpec 对 spec/ 做结构/引用/契约校验(具体 CLI 参数以 openspec --help 为准) - npm run spec:validate调用 OpenSpec 对 spec/ 做结构/引用/契约校验(具体 CLI 参数以 openspec --help 为准)
- 若为 API - 若为 API
- 必须在实现层提供请求/响应校验或至少在测试阶段进行契约校验。 - 必须在实现层提供请求/响应校验或至少在测试阶段进行契约校验。
- 鼓励(非强制)从 OpenAPI 生成 client/server stub 或生成类型定义但不得改变“JS 为主语言”的前提。 - 鼓励(非强制)从 OpenAPI 生成 client/server stub 或生成类型定义但不得改变“JS 为主语言”的前提。
4. 工程结构约束(建议默认) 4. 工程结构约束(建议默认)
AI 创建项目时,默认使用以下结构;如项目类型不适用,可在不违背约束的前提下做最小调整。 AI 创建项目时,默认使用以下结构;如项目类型不适用,可在不违背约束的前提下做最小调整。
- spec/OpenSpec 规范OpenAPI 或其他规格说明) - spec/OpenSpec 规范OpenAPI 或其他规格说明)
- src/:源代码 - src/:源代码
- tests/:测试 - tests/:测试
- scripts/:工程脚本(构建/校验/生成等) - scripts/:工程脚本(构建/校验/生成等)
- README.md必须包含运行、测试、规范使用方式 - README.md必须包含运行、测试、规范使用方式
5. 质量与交付约束(强制) 5. 质量与交付约束(强制)
- 必须提供基础脚本: - 必须提供基础脚本:
- npm run dev如可交互开发 - npm run dev如可交互开发
- npm run build如需要构建 - npm run build如需要构建
- npm run test - npm run test
- npm run lint - npm run lint
- 变更要求: - 变更要求:
- 修改实现时同步更新 spec/ 与测试。 - 修改实现时同步更新 spec/ 与测试。
- 不得只改实现不改规范;也不得只改规范不改实现。 - 不得只改实现不改规范;也不得只改规范不改实现。
6. AI 行为约束(强制) 6. AI 行为约束(强制)
- 若用户需求与本文件冲突: - 若用户需求与本文件冲突:
- 先指出冲突点,并请求用户确认是否允许偏离约束。 - 先指出冲突点,并请求用户确认是否允许偏离约束。
- 未明确要求时: - 未明确要求时:
- 不引入与约束无关的“额外页面/功能/组件/花哨配置”。 - 不引入与约束无关的“额外页面/功能/组件/花哨配置”。
- 保持最小可用、可验证、可维护的实现。 - 保持最小可用、可验证、可维护的实现。

View File

@@ -1,12 +1,12 @@
基于框架约束我想开发一个浏览器书签网站使用ui-ux-pro-max来美化前端界面手机和电脑端都能完美适配有一个简单的后台管理只管理账号邮箱密码即可并且只有管理能看见这个管理页面。这个浏览器书签不登录也可以使用但是只会把记录记在浏览器本地 localStorage 里,登录了才记入数据库(登录时把 localStorage 里的都加载进数据库。所以这就需要书签内容本身可以分类同一个书签既可以属于公开也可以属于私有每个用户的私有书签我可以通过管理员账号在账号管理里都能看见和操作。并且所有用户均可以从浏览器导出书签HTML文件然后再导入到这个网站里参考我导出的书签样本的HTML文件也可以从这个网站导出书签为HTML供其他浏览器使用。 基于框架约束我想开发一个浏览器书签网站使用ui-ux-pro-max来美化前端界面手机和电脑端都能完美适配有一个简单的后台管理只管理账号邮箱密码即可并且只有管理能看见这个管理页面。这个浏览器书签不登录也可以使用但是只会把记录记在浏览器本地 localStorage 里,登录了才记入数据库(登录时把 localStorage 里的都加载进数据库。所以这就需要书签内容本身可以分类同一个书签既可以属于公开也可以属于私有每个用户的私有书签我可以通过管理员账号在账号管理里都能看见和操作。并且所有用户均可以从浏览器导出书签HTML文件然后再导入到这个网站里参考我导出的书签样本的HTML文件也可以从这个网站导出书签为HTML供其他浏览器使用。
# 数据库连接参考 # 数据库连接参考
数据库使用 PGsql只能使用后端访问。必须写一个 `.env` 配置文件来处理(请勿在仓库中保存明文密码)。 数据库使用 PGsql只能使用后端访问。必须写一个 `.env` 配置文件来处理(请勿在仓库中保存明文密码)。
- 开发环境:`192.168.3.56:65432` - 开发环境:`192.168.3.56:65432`
- 部署环境:`127.0.0.1`(端口等配置以 `.env` 为准) - 部署环境:`127.0.0.1`(端口等配置以 `.env` 为准)
我需要你帮我完成的:前端完整的页面,后端完整的方法,所有的数据库操作和结构。 我需要你帮我完成的:前端完整的页面,后端完整的方法,所有的数据库操作和结构。
要求数据库必须优化命名严格规范小驼峰命名法在关键字段必须加入索引目前需求需要2个类型的表首先是用户库保存用户所有信息。其次是书签需要分多个表一张表存放书签类别类似具体书签的文件夹包含类别名称和其他相关信息等一张表存放书签内容包含连接和名称等必要信息这两张表需要进行连表索引。 要求数据库必须优化命名严格规范小驼峰命名法在关键字段必须加入索引目前需求需要2个类型的表首先是用户库保存用户所有信息。其次是书签需要分多个表一张表存放书签类别类似具体书签的文件夹包含类别名称和其他相关信息等一张表存放书签内容包含连接和名称等必要信息这两张表需要进行连表索引。

View File

@@ -1,131 +1,131 @@
# 验收清单持久排序folders + bookmarks+ 触屏拖拽 # 验收清单持久排序folders + bookmarks+ 触屏拖拽
> 目标:文件夹与书签都能在 PC/手机端拖动排序,刷新后顺序保持;同时无滚动条但仍可滚动;根目录(未分组)视为一个“虚拟文件夹组”。 > 目标:文件夹与书签都能在 PC/手机端拖动排序,刷新后顺序保持;同时无滚动条但仍可滚动;根目录(未分组)视为一个“虚拟文件夹组”。
## 0. 前置条件 ## 0. 前置条件
- 已启动 Postgres`.env` 配置正确(支持放在 repo 根目录 `.env``apps/server/.env`)。 - 已启动 Postgres`.env` 配置正确(支持放在 repo 根目录 `.env``apps/server/.env`)。
- 推荐先重建数据库(开发阶段方便确保 schema 一致)。 - 推荐先重建数据库(开发阶段方便确保 schema 一致)。
### 0.1(可选)确认 `.env` ### 0.1(可选)确认 `.env`
参考根目录 `.env.example` 参考根目录 `.env.example`
关键项: 关键项:
- `DATABASE_HOST/DATABASE_PORT/DATABASE_NAME/DATABASE_USER/DATABASE_PASSWORD` - `DATABASE_HOST/DATABASE_PORT/DATABASE_NAME/DATABASE_USER/DATABASE_PASSWORD`
- `AUTH_JWT_SECRET` - `AUTH_JWT_SECRET`
- `ADMIN_EMAIL`(可选,用于验收管理端) - `ADMIN_EMAIL`(可选,用于验收管理端)
## 1. 重建数据库(强烈建议) ## 1. 重建数据库(强烈建议)
> 注意:此操作会 DROP 表并清空数据,仅用于开发环境。 > 注意:此操作会 DROP 表并清空数据,仅用于开发环境。
在仓库根目录执行: 在仓库根目录执行:
- `npm -w apps/server run db:reset` - `npm -w apps/server run db:reset`
预期:命令成功结束;下次启动服务不会再出现缺列(例如 `sort_order`)相关报错。 预期:命令成功结束;下次启动服务不会再出现缺列(例如 `sort_order`)相关报错。
### 1.1(可选)用 SQL 验证列存在 ### 1.1(可选)用 SQL 验证列存在
- `\d bookmarks` 应包含 `sort_order` - `\d bookmarks` 应包含 `sort_order`
- `\d bookmark_folders` 应包含 `sort_order` - `\d bookmark_folders` 应包含 `sort_order`
或执行: 或执行:
```sql ```sql
select column_name select column_name
from information_schema.columns from information_schema.columns
where table_schema=current_schema() where table_schema=current_schema()
and table_name='bookmarks' and table_name='bookmarks'
order by column_name; order by column_name;
``` ```
## 2. 启动server + web ## 2. 启动server + web
在仓库根目录各开一个终端: 在仓库根目录各开一个终端:
- Server`npm -w apps/server run dev` - Server`npm -w apps/server run dev`
- Web`npm -w apps/web run dev` - Web`npm -w apps/web run dev`
预期: 预期:
- Server 健康检查:`GET http://localhost:3001/health` 返回 `{ ok: true }` - Server 健康检查:`GET http://localhost:3001/health` 返回 `{ ok: true }`
- Web 能正常访问并登录。 - Web 能正常访问并登录。
## 3. UI/交互验收PC ## 3. UI/交互验收PC
### 3.1 “无滚动条但可滚动” ### 3.1 “无滚动条但可滚动”
- 进入 Web 页面(任意长列表页) - 进入 Web 页面(任意长列表页)
- 鼠标滚轮/触控板滚动 - 鼠标滚轮/触控板滚动
预期:页面可以滚动,但看不到滚动条(侧边/底部不出现条)。 预期:页面可以滚动,但看不到滚动条(侧边/底部不出现条)。
### 3.2 “我的书签”展开/折叠 ### 3.2 “我的书签”展开/折叠
- 进入 `/my` - 进入 `/my`
- 点击任意文件夹头部 - 点击任意文件夹头部
预期:能展开/收起;不会出现“点击没反应”。 预期:能展开/收起;不会出现“点击没反应”。
### 3.3 文件夹拖拽排序(同父级) ### 3.3 文件夹拖拽排序(同父级)
- 保证至少有 2 个同级文件夹(同一个 parent 下) - 保证至少有 2 个同级文件夹(同一个 parent 下)
-`/my` 使用文件夹右侧的拖拽柄(⋮⋮)拖动排序 -`/my` 使用文件夹右侧的拖拽柄(⋮⋮)拖动排序
预期: 预期:
- 能拖动、松手后顺序变化 - 能拖动、松手后顺序变化
- 刷新页面后顺序保持 - 刷新页面后顺序保持
约束预期: 约束预期:
- 不允许跨父级拖动(不同 parent 的文件夹不能混排) - 不允许跨父级拖动(不同 parent 的文件夹不能混排)
### 3.4 书签拖拽排序(根目录 + 文件夹内) ### 3.4 书签拖拽排序(根目录 + 文件夹内)
- 在“未分组(根目录)”组内拖动书签排序 - 在“未分组(根目录)”组内拖动书签排序
- 展开某个文件夹,在该文件夹内拖动书签排序 - 展开某个文件夹,在该文件夹内拖动书签排序
预期: 预期:
- 两处都能拖动排序 - 两处都能拖动排序
- 刷新页面后顺序保持 - 刷新页面后顺序保持
- 拖拽柄拖动不会误触打开链接 - 拖拽柄拖动不会误触打开链接
### 3.5 搜索模式禁用排序 ### 3.5 搜索模式禁用排序
-`/my` 的搜索框输入关键字(进入过滤状态) -`/my` 的搜索框输入关键字(进入过滤状态)
- 尝试拖动(文件夹/书签) - 尝试拖动(文件夹/书签)
预期:拖拽排序不生效(避免搜索时误操作导致重排)。 预期:拖拽排序不生效(避免搜索时误操作导致重排)。
## 4. 触屏验收(手机/模拟器) ## 4. 触屏验收(手机/模拟器)
- 打开 `/my` - 打开 `/my`
- 长按拖拽柄(⋮⋮)并移动 - 长按拖拽柄(⋮⋮)并移动
预期: 预期:
- 文件夹可拖动排序(同父级) - 文件夹可拖动排序(同父级)
- 书签可拖动排序(根目录/文件夹内) - 书签可拖动排序(根目录/文件夹内)
- 刷新后顺序保持 - 刷新后顺序保持
## 5. 管理端验收(可选,需要 ADMIN_EMAIL ## 5. 管理端验收(可选,需要 ADMIN_EMAIL
### 5.1 设置管理员 ### 5.1 设置管理员
- `.env` 设置 `ADMIN_EMAIL=你用来登录的邮箱` - `.env` 设置 `ADMIN_EMAIL=你用来登录的邮箱`
- 重新启动 server - 重新启动 server
### 5.2 访问管理端 ### 5.2 访问管理端
- 用该邮箱登录 - 用该邮箱登录
- 打开 `/admin` - 打开 `/admin`
预期: 预期:
- 能看到用户列表 - 能看到用户列表
- 选择用户后,能看到该用户的文件夹与书签(按 sortOrder 展示) - 选择用户后,能看到该用户的文件夹与书签(按 sortOrder 展示)
- 删除书签/删除文件夹/复制书签到管理员账号能正常工作 - 删除书签/删除文件夹/复制书签到管理员账号能正常工作
## 6. 常见失败点与定位 ## 6. 常见失败点与定位
- 拖拽接口返回 409数据库 schema 未包含 `sort_order`,请先跑 `npm -w apps/server run db:migrate` 或直接 `db:reset` - 拖拽接口返回 409数据库 schema 未包含 `sort_order`,请先跑 `npm -w apps/server run db:migrate` 或直接 `db:reset`
- 拖拽后刷新不保存:检查 server 日志是否收到 `/folders/reorder``/bookmarks/reorder`;以及 web 是否使用同一个 `VITE_SERVER_BASE_URL` - 拖拽后刷新不保存:检查 server 日志是否收到 `/folders/reorder``/bookmarks/reorder`;以及 web 是否使用同一个 `VITE_SERVER_BASE_URL`
- “点击文件夹没反应”:优先查看浏览器控制台是否有运行时错误(应已修复模板误用 `.value` 的问题)。 - “点击文件夹没反应”:优先查看浏览器控制台是否有运行时错误(应已修复模板误用 `.value` 的问题)。

View File

@@ -1,24 +1,24 @@
# Design: Persistent ordering + touch-friendly DnD # Design: Persistent ordering + touch-friendly DnD
## Database ## Database
- Add `sort_order integer not null default 0` to `bookmarks`. - Add `sort_order integer not null default 0` to `bookmarks`.
- Add indexes to support ordered listing: - Add indexes to support ordered listing:
- `(user_id, folder_id, sort_order)` - `(user_id, folder_id, sort_order)`
## API ## API
- Extend `Bookmark` DTO/schema with `sortOrder`. - Extend `Bookmark` DTO/schema with `sortOrder`.
- Add `POST /bookmarks/reorder` similar to existing `/folders/reorder`: - Add `POST /bookmarks/reorder` similar to existing `/folders/reorder`:
- Input: `{ folderId: uuid|null, orderedIds: uuid[] }` - Input: `{ folderId: uuid|null, orderedIds: uuid[] }`
- Validates `orderedIds` is a permutation of all bookmarks for that user+folder (excluding deleted). - Validates `orderedIds` is a permutation of all bookmarks for that user+folder (excluding deleted).
- Transactionally updates `sort_order` for each id. - Transactionally updates `sort_order` for each id.
## Web UI ## Web UI
- Replace native HTML5 drag/drop with a touch-capable approach. - Replace native HTML5 drag/drop with a touch-capable approach.
- Implementation choice: `sortablejs` (small, proven, touch-friendly). - Implementation choice: `sortablejs` (small, proven, touch-friendly).
- Bind Sortable to: - Bind Sortable to:
- Folder header list (per parent group) for folder ordering. - Folder header list (per parent group) for folder ordering.
- Each open folders bookmark list for bookmark ordering. - Each open folders bookmark list for bookmark ordering.
- Root group is rendered as a first-class group and can also be reordered. - Root group is rendered as a first-class group and can also be reordered.
## Compatibility ## Compatibility
- If the DB schema lacks ordering columns (fresh/old DB), endpoints should return a clear 409 prompting `db:migrate`. - If the DB schema lacks ordering columns (fresh/old DB), endpoints should return a clear 409 prompting `db:migrate`.

View File

@@ -1,18 +1,18 @@
# Change: Add persistent drag-and-drop sorting (folders + bookmarks) # Change: Add persistent drag-and-drop sorting (folders + bookmarks)
## Why ## Why
Users need to reorder folders and bookmarks via drag-and-drop (including mobile/touch) and have that order persist across reloads. Current HTML5 drag/drop is unreliable on mobile and ordering is not stored for bookmarks. Users need to reorder folders and bookmarks via drag-and-drop (including mobile/touch) and have that order persist across reloads. Current HTML5 drag/drop is unreliable on mobile and ordering is not stored for bookmarks.
## What Changes ## What Changes
- Add persistent ordering for bookmarks (new DB column and API endpoint to reorder within a folder). - Add persistent ordering for bookmarks (new DB column and API endpoint to reorder within a folder).
- Use a touch-friendly drag-and-drop implementation in the web UI for: - Use a touch-friendly drag-and-drop implementation in the web UI for:
- Reordering folders within the same parent. - Reordering folders within the same parent.
- Reordering bookmarks within the same folder. - Reordering bookmarks within the same folder.
- Keep the root group (no folder) as a first-class group in the UI. - Keep the root group (no folder) as a first-class group in the UI.
## Impact ## Impact
- Affected specs: API (OpenAPI-backed) - Affected specs: API (OpenAPI-backed)
- Affected code: - Affected code:
- Server: migrations, bookmarks routes, admin routes, row DTO mapping - Server: migrations, bookmarks routes, admin routes, row DTO mapping
- Web: MyPage and AdminPage UI ordering and drag/drop - Web: MyPage and AdminPage UI ordering and drag/drop
- OpenAPI: Bookmark schema and reorder endpoint - OpenAPI: Bookmark schema and reorder endpoint

View File

@@ -1,35 +1,35 @@
## ADDED Requirements ## ADDED Requirements
### Requirement: Folder ordering persistence ### Requirement: Folder ordering persistence
The system SHALL persist folder ordering per user per parent folder. The system SHALL persist folder ordering per user per parent folder.
#### Scenario: List folders returns stable ordered result #### Scenario: List folders returns stable ordered result
- **GIVEN** an authenticated user - **GIVEN** an authenticated user
- **WHEN** the user calls `GET /folders` - **WHEN** the user calls `GET /folders`
- **THEN** the server returns folders ordered by `(parentId, sortOrder, name)` - **THEN** the server returns folders ordered by `(parentId, sortOrder, name)`
#### Scenario: Reorder folders within the same parent #### Scenario: Reorder folders within the same parent
- **GIVEN** an authenticated user - **GIVEN** an authenticated user
- **WHEN** the user calls `POST /folders/reorder` with `parentId` and `orderedIds` - **WHEN** the user calls `POST /folders/reorder` with `parentId` and `orderedIds`
- **THEN** the server persists the new order and returns `{ ok: true }` - **THEN** the server persists the new order and returns `{ ok: true }`
### Requirement: Bookmark ordering persistence ### Requirement: Bookmark ordering persistence
The system SHALL persist bookmark ordering per user per folder. The system SHALL persist bookmark ordering per user per folder.
#### Scenario: List my bookmarks returns stable ordered result #### Scenario: List my bookmarks returns stable ordered result
- **GIVEN** an authenticated user - **GIVEN** an authenticated user
- **WHEN** the user calls `GET /bookmarks` - **WHEN** the user calls `GET /bookmarks`
- **THEN** the server returns bookmarks ordered by `(folderId, sortOrder, updatedAt desc)` - **THEN** the server returns bookmarks ordered by `(folderId, sortOrder, updatedAt desc)`
#### Scenario: Reorder bookmarks within the same folder #### Scenario: Reorder bookmarks within the same folder
- **GIVEN** an authenticated user - **GIVEN** an authenticated user
- **WHEN** the user calls `POST /bookmarks/reorder` with `folderId` and `orderedIds` - **WHEN** the user calls `POST /bookmarks/reorder` with `folderId` and `orderedIds`
- **THEN** the server persists the new order and returns `{ ok: true }` - **THEN** the server persists the new order and returns `{ ok: true }`
### Requirement: Root group treated consistently ### Requirement: Root group treated consistently
The system SHALL treat `folderId=null` bookmarks as belonging to the root group. The system SHALL treat `folderId=null` bookmarks as belonging to the root group.
#### Scenario: Reorder root-group bookmarks #### Scenario: Reorder root-group bookmarks
- **GIVEN** an authenticated user - **GIVEN** an authenticated user
- **WHEN** the user calls `POST /bookmarks/reorder` with `folderId=null` - **WHEN** the user calls `POST /bookmarks/reorder` with `folderId=null`
- **THEN** the server reorders root-group bookmarks and returns `{ ok: true }` - **THEN** the server reorders root-group bookmarks and returns `{ ok: true }`

View File

@@ -1,12 +1,12 @@
## 1. Implementation ## 1. Implementation
- [ ] Add DB support for bookmark ordering (migration + init schema) - [ ] Add DB support for bookmark ordering (migration + init schema)
- [ ] Expose bookmark ordering in DTOs and OpenAPI schema - [ ] Expose bookmark ordering in DTOs and OpenAPI schema
- [ ] Add API endpoint to reorder bookmarks within the same folder - [ ] Add API endpoint to reorder bookmarks within the same folder
- [ ] Ensure list endpoints return folders/bookmarks in stable order (parent+sortOrder, etc.) - [ ] Ensure list endpoints return folders/bookmarks in stable order (parent+sortOrder, etc.)
- [ ] Implement touch-friendly drag sorting in Web UI for folders and bookmarks - [ ] Implement touch-friendly drag sorting in Web UI for folders and bookmarks
- [ ] Treat root group (folderId null) as a first-class group for display and bookmark reorder - [ ] Treat root group (folderId null) as a first-class group for display and bookmark reorder
- [ ] Add basic verification steps (build + manual smoke checklist) - [ ] Add basic verification steps (build + manual smoke checklist)
## 2. Spec Updates ## 2. Spec Updates
- [ ] Update OpenAPI contract for bookmark sortOrder and reorder endpoint - [ ] Update OpenAPI contract for bookmark sortOrder and reorder endpoint
- [ ] Update OpenSpec API capability delta requirements - [ ] Update OpenSpec API capability delta requirements

View File

@@ -1,65 +1,65 @@
# Capability: API (OpenAPI-backed) # Capability: API (OpenAPI-backed)
## Purpose ## Purpose
Describe the core HTTP API behavior and constraints. The OpenAPI 3.1 contract lives in `spec/openapi.yaml`. Describe the core HTTP API behavior and constraints. The OpenAPI 3.1 contract lives in `spec/openapi.yaml`.
## Requirements ## Requirements
### Requirement: Health check ### Requirement: Health check
The system SHALL expose a health endpoint for availability checks. The system SHALL expose a health endpoint for availability checks.
#### Scenario: Health endpoint returns OK #### Scenario: Health endpoint returns OK
- **WHEN** a client calls `GET /health` - **WHEN** a client calls `GET /health`
- **THEN** the server returns `200` - **THEN** the server returns `200`
### Requirement: Authentication ### Requirement: Authentication
The system SHALL support email+password registration and login. The system SHALL support email+password registration and login.
#### Scenario: Register then login #### Scenario: Register then login
- **WHEN** a user registers with a valid email and password - **WHEN** a user registers with a valid email and password
- **THEN** the server returns a JWT token - **THEN** the server returns a JWT token
- **WHEN** the user logs in with the same credentials - **WHEN** the user logs in with the same credentials
- **THEN** the server returns a JWT token - **THEN** the server returns a JWT token
### Requirement: Public bookmarks visibility ### Requirement: Public bookmarks visibility
The system SHALL allow anonymous users to view public bookmarks. The system SHALL allow anonymous users to view public bookmarks.
#### Scenario: List public bookmarks without auth #### Scenario: List public bookmarks without auth
- **WHEN** a client calls `GET /bookmarks/public` without a token - **WHEN** a client calls `GET /bookmarks/public` without a token
- **THEN** the server returns `200` and a list of bookmarks - **THEN** the server returns `200` and a list of bookmarks
### Requirement: Private bookmarks visibility ### Requirement: Private bookmarks visibility
The system SHALL restrict private bookmark data to authenticated users. The system SHALL restrict private bookmark data to authenticated users.
#### Scenario: List my bookmarks requires auth #### Scenario: List my bookmarks requires auth
- **WHEN** a client calls `GET /bookmarks` without a token - **WHEN** a client calls `GET /bookmarks` without a token
- **THEN** the server returns an auth error - **THEN** the server returns an auth error
- **WHEN** a client calls `GET /bookmarks` with a valid token - **WHEN** a client calls `GET /bookmarks` with a valid token
- **THEN** the server returns `200` and the user's bookmarks - **THEN** the server returns `200` and the user's bookmarks
### Requirement: Sync (LWW) ### Requirement: Sync (LWW)
The system SHALL support last-write-wins (LWW) synchronization for folders and bookmarks. The system SHALL support last-write-wins (LWW) synchronization for folders and bookmarks.
#### Scenario: Push local changes then pull #### Scenario: Push local changes then pull
- **WHEN** an authenticated client calls `POST /sync/push` with folders/bookmarks - **WHEN** an authenticated client calls `POST /sync/push` with folders/bookmarks
- **THEN** the server stores the items using LWW semantics - **THEN** the server stores the items using LWW semantics
- **WHEN** the client calls `GET /sync/pull` - **WHEN** the client calls `GET /sync/pull`
- **THEN** the server returns folders/bookmarks and `serverTime` - **THEN** the server returns folders/bookmarks and `serverTime`
### Requirement: Admin user management (email-based) ### Requirement: Admin user management (email-based)
The system SHALL treat exactly one configured email as an administrator and allow that user to manage/view users. The system SHALL treat exactly one configured email as an administrator and allow that user to manage/view users.
#### Scenario: Non-admin cannot access admin APIs #### Scenario: Non-admin cannot access admin APIs
- **GIVEN** an authenticated user whose email is not equal to `ADMIN_EMAIL` - **GIVEN** an authenticated user whose email is not equal to `ADMIN_EMAIL`
- **WHEN** the user calls `GET /admin/users` - **WHEN** the user calls `GET /admin/users`
- **THEN** the server returns a 403 error - **THEN** the server returns a 403 error
#### Scenario: Admin can list users #### Scenario: Admin can list users
- **GIVEN** an authenticated user whose email equals `ADMIN_EMAIL` - **GIVEN** an authenticated user whose email equals `ADMIN_EMAIL`
- **WHEN** the user calls `GET /admin/users` - **WHEN** the user calls `GET /admin/users`
- **THEN** the server returns `200` and a list of users - **THEN** the server returns `200` and a list of users
#### Scenario: Admin can view a user's bookmarks #### Scenario: Admin can view a user's bookmarks
- **GIVEN** an authenticated admin user - **GIVEN** an authenticated admin user
- **WHEN** the admin calls `GET /admin/users/{id}/bookmarks` - **WHEN** the admin calls `GET /admin/users/{id}/bookmarks`
- **THEN** the server returns `200` and that user's bookmarks - **THEN** the server returns `200` and that user's bookmarks

8300
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,25 +1,25 @@
{ {
"name": "browser-bookmark", "name": "browser-bookmark",
"private": true, "private": true,
"workspaces": [ "workspaces": [
"apps/*", "apps/*",
"packages/*" "packages/*"
], ],
"devDependencies": { "devDependencies": {
"concurrently": "^9.1.2" "concurrently": "^9.1.2"
}, },
"scripts": { "scripts": {
"dev": "concurrently -n server,web -c cyan,magenta \"npm run -w apps/server dev\" \"npm run -w apps/web dev\"", "dev": "concurrently -n server,web -c cyan,magenta \"npm run -w apps/server dev\" \"npm run -w apps/web dev\"",
"dev:web": "npm run -w apps/web dev", "dev:web": "npm run -w apps/web dev",
"dev:server": "npm run -w apps/server dev", "dev:server": "npm run -w apps/server dev",
"dev:extension": "npm run -w apps/extension dev", "dev:extension": "npm run -w apps/extension dev",
"build": "npm run -w apps/server build && npm run -w apps/web build && npm run -w apps/extension build", "build": "npm run -w apps/server build && npm run -w apps/web build && npm run -w apps/extension build",
"test": "npm run -w apps/server test && npm run -w apps/web test && npm run -w apps/extension test", "test": "npm run -w apps/server test && npm run -w apps/web test && npm run -w apps/extension test",
"lint": "npm run -w apps/server lint && npm run -w apps/web lint && npm run -w apps/extension lint", "lint": "npm run -w apps/server lint && npm run -w apps/web lint && npm run -w apps/extension lint",
"spec:lint": "openspec validate --specs --no-interactive", "spec:lint": "openspec validate --specs --no-interactive",
"spec:validate": "openspec validate --specs --strict --no-interactive" "spec:validate": "openspec validate --specs --strict --no-interactive"
}, },
"engines": { "engines": {
"node": ">=22" "node": ">=22"
} }
} }

View File

@@ -1 +1 @@
export * from "./src/index.js"; export * from "./src/index.js";

View File

@@ -1,9 +1,9 @@
{ {
"name": "@browser-bookmark/shared", "name": "@browser-bookmark/shared",
"private": true, "private": true,
"type": "module", "type": "module",
"version": "0.1.0", "version": "0.1.0",
"exports": { "exports": {
".": "./index.js" ".": "./index.js"
} }
} }

View File

@@ -1,77 +1,77 @@
// Netscape Bookmark file format parser (Chrome/Edge export) // Netscape Bookmark file format parser (Chrome/Edge export)
// Parses <DL>/<DT><H3> folders and <DT><A> bookmarks. // Parses <DL>/<DT><H3> folders and <DT><A> bookmarks.
export function parseNetscapeBookmarkHtml(html) { export function parseNetscapeBookmarkHtml(html) {
// Minimal, dependency-free parser using DOMParser (works in browsers). // Minimal, dependency-free parser using DOMParser (works in browsers).
// For Node.js, server will use a separate HTML parser. // For Node.js, server will use a separate HTML parser.
const parser = new DOMParser(); const parser = new DOMParser();
const doc = parser.parseFromString(html, "text/html"); const doc = parser.parseFromString(html, "text/html");
const rootDl = doc.querySelector("dl"); const rootDl = doc.querySelector("dl");
if (!rootDl) return { folders: [], bookmarks: [] }; if (!rootDl) return { folders: [], bookmarks: [] };
const folders = []; const folders = [];
const bookmarks = []; const bookmarks = [];
let folderIdSeq = 1; let folderIdSeq = 1;
function normText(s) { function normText(s) {
return String(s || "").replace(/\s+/g, " ").trim(); return String(s || "").replace(/\s+/g, " ").trim();
} }
// Collect <DT> nodes that belong to the current <DL> level. // Collect <DT> nodes that belong to the current <DL> level.
// Chrome/Edge exported HTML often uses `<DL><p>` and browsers may wrap // Chrome/Edge exported HTML often uses `<DL><p>` and browsers may wrap
// subsequent nodes under <p> or other wrapper elements. // subsequent nodes under <p> or other wrapper elements.
function collectLevelDt(container) { function collectLevelDt(container) {
const out = []; const out = [];
const els = Array.from(container.children || []); const els = Array.from(container.children || []);
for (const el of els) { for (const el of els) {
const tag = el.tagName?.toLowerCase(); const tag = el.tagName?.toLowerCase();
if (!tag) continue; if (!tag) continue;
if (tag === "dt") { if (tag === "dt") {
out.push(el); out.push(el);
continue; continue;
} }
if (tag === "dl") { if (tag === "dl") {
// nested list belongs to the previous <DT> // nested list belongs to the previous <DT>
continue; continue;
} }
out.push(...collectLevelDt(el)); out.push(...collectLevelDt(el));
} }
return out; return out;
} }
// Find the nested <DL> that belongs to a <DT>, even if <DT> is wrapped (e.g. inside <p>). // Find the nested <DL> that belongs to a <DT>, even if <DT> is wrapped (e.g. inside <p>).
function findNextDlForDt(dt, stopDl) { function findNextDlForDt(dt, stopDl) {
let cur = dt; let cur = dt;
while (cur && cur !== stopDl) { while (cur && cur !== stopDl) {
const next = cur.nextElementSibling; const next = cur.nextElementSibling;
if (next && next.tagName?.toLowerCase() === "dl") return next; if (next && next.tagName?.toLowerCase() === "dl") return next;
cur = cur.parentElement; cur = cur.parentElement;
} }
return null; return null;
} }
function walkDl(dl, parentFolderId) { function walkDl(dl, parentFolderId) {
const dts = collectLevelDt(dl); const dts = collectLevelDt(dl);
for (const node of dts) { for (const node of dts) {
const h3 = node.querySelector("h3"); const h3 = node.querySelector("h3");
const a = node.querySelector("a"); const a = node.querySelector("a");
const nestedDl = node.querySelector("dl"); const nestedDl = node.querySelector("dl");
const nextDl = nestedDl || findNextDlForDt(node, dl); const nextDl = nestedDl || findNextDlForDt(node, dl);
if (h3) { if (h3) {
const id = String(folderIdSeq++); const id = String(folderIdSeq++);
const name = normText(h3.textContent || ""); const name = normText(h3.textContent || "");
folders.push({ id, parentFolderId: parentFolderId ?? null, name }); folders.push({ id, parentFolderId: parentFolderId ?? null, name });
if (nextDl) walkDl(nextDl, id); if (nextDl) walkDl(nextDl, id);
} else if (a) { } else if (a) {
const title = normText(a.textContent || ""); const title = normText(a.textContent || "");
const url = a.getAttribute("href") || ""; const url = a.getAttribute("href") || "";
bookmarks.push({ parentFolderId: parentFolderId ?? null, title, url }); bookmarks.push({ parentFolderId: parentFolderId ?? null, title, url });
} }
} }
} }
walkDl(rootDl, null); walkDl(rootDl, null);
return { folders, bookmarks }; return { folders, bookmarks };
} }

View File

@@ -1,49 +1,49 @@
export function normalizeUrl(input) { export function normalizeUrl(input) {
try { try {
const url = new URL(input); const url = new URL(input);
url.hash = ""; url.hash = "";
url.protocol = url.protocol.toLowerCase(); url.protocol = url.protocol.toLowerCase();
url.hostname = url.hostname.toLowerCase(); url.hostname = url.hostname.toLowerCase();
// Remove default ports // Remove default ports
if ((url.protocol === "http:" && url.port === "80") || (url.protocol === "https:" && url.port === "443")) { if ((url.protocol === "http:" && url.port === "80") || (url.protocol === "https:" && url.port === "443")) {
url.port = ""; url.port = "";
} }
// Trim trailing slash on pathname (but keep root '/') // Trim trailing slash on pathname (but keep root '/')
if (url.pathname.length > 1 && url.pathname.endsWith("/")) { if (url.pathname.length > 1 && url.pathname.endsWith("/")) {
url.pathname = url.pathname.slice(0, -1); url.pathname = url.pathname.slice(0, -1);
} }
// Drop common tracking params // Drop common tracking params
const trackingPrefixes = ["utm_", "spm", "gclid", "fbclid"]; const trackingPrefixes = ["utm_", "spm", "gclid", "fbclid"];
for (const key of [...url.searchParams.keys()]) { for (const key of [...url.searchParams.keys()]) {
const lowerKey = key.toLowerCase(); const lowerKey = key.toLowerCase();
if (trackingPrefixes.some((p) => lowerKey.startsWith(p))) { if (trackingPrefixes.some((p) => lowerKey.startsWith(p))) {
url.searchParams.delete(key); url.searchParams.delete(key);
} }
} }
// Sort params for stable output // Sort params for stable output
const sorted = [...url.searchParams.entries()].sort(([a], [b]) => a.localeCompare(b)); const sorted = [...url.searchParams.entries()].sort(([a], [b]) => a.localeCompare(b));
url.search = ""; url.search = "";
for (const [k, v] of sorted) url.searchParams.append(k, v); for (const [k, v] of sorted) url.searchParams.append(k, v);
return url.toString(); return url.toString();
} catch { } catch {
return input; return input;
} }
} }
export function computeUrlHash(normalizedUrl) { export function computeUrlHash(normalizedUrl) {
// Lightweight hash (non-crypto) for dedupe key; server may replace with crypto later. // Lightweight hash (non-crypto) for dedupe key; server may replace with crypto later.
let hash = 2166136261; let hash = 2166136261;
for (let i = 0; i < normalizedUrl.length; i++) { for (let i = 0; i < normalizedUrl.length; i++) {
hash ^= normalizedUrl.charCodeAt(i); hash ^= normalizedUrl.charCodeAt(i);
hash = Math.imul(hash, 16777619); hash = Math.imul(hash, 16777619);
} }
return (hash >>> 0).toString(16); return (hash >>> 0).toString(16);
} }
export { parseNetscapeBookmarkHtml } from "./bookmarkHtml.js"; export { parseNetscapeBookmarkHtml } from "./bookmarkHtml.js";

View File

@@ -1,4 +1,4 @@
{ {
"private": true, "private": true,
"type": "module" "type": "module"
} }

File diff suppressed because it is too large Load Diff