generateHtml property

  1. @override
Tag Function(Map args)? generateHtml
getter/setter pairoverride

Implementation

@override
Tag Function(Map args)? generateHtml = (Map args) {
  int statusCode = args['status'] ?? 500;
  String title = args['title'] ?? 'Error $statusCode';
  String error = (args['error'] ?? '').toString();
  List stack = args['stack'] ?? [];

  return ArrayTag(
    children: [
      $Doctype(),
      $Html(
        children: [
          $Head(children: [
            $Meta(attrs: {'charset': 'utf-8'}),
            $Meta(attrs: {
              'http-equiv': 'X-UA-Compatible',
              'content': 'IE=edge'
            }),
            $Meta(attrs: {
              'name': 'viewport',
              'content': 'width=device-width, initial-scale=1'
            }),
            $Title(children: [$Text(title)]),
            $Style(children: [
              $Raw("""
          /* ---------- Modern Error Page Styles ---------- */
          :root {
            --color-bg: #0f1115;
            --color-bg-alt: #181b21;
            --color-surface: #1f242c;
            --color-border: #2c333d;
            --color-accent: #3da9fc;
            --color-accent-glow: 75, 170, 255;
            --color-error: #ff4c60;
            --color-error-soft: #ff4c6030;
            --color-warn: #ffc861;
            --color-text: #e6ebf1;
            --color-text-dim: #9aa4b1;
            --row-alt: #1b2026;
            --row-hover: #242b33;
            --bg-global: radial-gradient(circle at 25% 15%, #1d2430, #0f1115 60%) fixed;
            --table-head-bg: linear-gradient(90deg, #252c34, #20252c);
            --table-head-text: var(--color-text-dim);
            --error-box-bg: linear-gradient(135deg, #2a1217, #361a1f 55%, #331319);
            --error-box-border: rgba(255,76,96,.45);
            --error-box-text: #ffd9de;
            --radius-sm: 6px;
            --radius-md: 12px;
            --radius-lg: 18px;
            --shadow-sm: 0 2px 4px -1px rgba(0,0,0,.4),0 1px 1px rgba(0,0,0,.25);
            --shadow-md: 0 4px 18px -2px rgba(0,0,0,.55),0 2px 4px rgba(0,0,0,.35);
            --gradient-accent: linear-gradient(135deg,#3da9fc,#6d67ff 60%,#b44cff);
            font-family: 'Courier New', Courier, 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', Arial, sans-serif;
          }

          /* Light theme overrides */
          body[data-theme='light'] {
            --color-bg: #fafbfc;
            --color-bg-alt: #ffffff;
            --color-surface: #ffffff;
            --color-border: #e1e8f0;
            --color-accent: #6366f1;
            --color-accent-glow: 99, 102, 241;
            --color-error: #ef4444;
            --color-error-soft: #fef2f2;
            --color-warn: #f59e0b;
            --color-text: #1f2937;
            --color-text-dim: #6b7280;
            --row-alt: #f8fafc;
            --row-hover: #f1f5f9;
            --bg-global: linear-gradient(135deg, #ffffff 0%, #f8fafc 25%, #f1f5f9 100%);
            --table-head-bg: linear-gradient(135deg, #f8fafc, #f1f5f9);
            --table-head-text: #374151;
            --error-box-bg: linear-gradient(135deg, #fef2f2, #fef7f7);
            --error-box-border: #fecaca;
            --error-box-text: #b91c1c;
            --gradient-accent: linear-gradient(135deg, #6366f1, #8b5cf6 60%, #ec4899);
            --shadow-sm: 0 1px 3px rgba(0,0,0,.08), 0 1px 2px rgba(0,0,0,.04);
            --shadow-md: 0 4px 16px rgba(0,0,0,.06), 0 2px 8px rgba(0,0,0,.04);
          }

          * { box-sizing: border-box; }

          body {
            margin: 0;
            padding: clamp(16px, 3vw, 40px);
            background: var(--bg-global);
            color: var(--color-text);
            -webkit-font-smoothing: antialiased;
            line-height: 1.45;
            transition: background .4s ease, color .25s ease;
          }

          h3 {
            margin: 0 0 .75rem;
            font-size: 2.3rem;
            font-weight: 600;
            letter-spacing: -1px;
            background: var(--gradient-accent);
            -webkit-background-clip: text;
            color: transparent;
            filter: drop-shadow(0 3px 8px rgba(var(--color-accent-glow), .35));
          }

          p { margin: 0 0 1.2rem; font-size: 1.05rem; color: var(--color-text-dim); }

          code, pre { font-family: 'Fira Mono', 'Source Code Pro', monospace; }

          .error-alert {
            display: block;
            background: var(--color-error-soft);
            border: 1px solid var(--color-error);
            border-radius: var(--radius-md);
            padding: 12px 16px;
            color: var(--color-error);
            font-size: .9rem;
            line-height: 1.4;
            margin: 0 0 1.5rem;
            overflow: auto;
            max-height: 260px;
            transition: background .4s ease, color .25s ease, border-color .4s ease;
          }

          .error-shell {
            max-width: 900px;
            margin: 0 auto;
            background: linear-gradient(145deg, var(--color-bg-alt), var(--color-surface));
            border: 1px solid var(--color-border);
            border-radius: var(--radius-lg);
            padding: clamp(24px, 3vw, 42px);
            box-shadow: var(--shadow-md);
            position: relative;
            overflow: hidden;
          }
          .error-shell::before, .error-shell::after {
            content: '';
            position: absolute;
            width: 420px; height: 420px;
            background: radial-gradient(circle at center, rgba(var(--color-accent-glow), .18), transparent 70%);
            top: -160px; right: -160px;
            pointer-events: none;
            mix-blend-mode: screen;
            filter: blur(8px);
          }
          .error-shell::after { top: auto; bottom: -180px; right: auto; left: -180px; }

          .badge-code {
            display: inline-flex;
            align-items: center;
            gap: .5ch;
            background: var(--color-error-soft);
            color: var(--color-error);
            border: 1px solid var(--color-error);
            font-weight: 600;
            padding: 4px 10px 4px 8px;
            border-radius: 100px;
            font-size: .85rem;
            letter-spacing: .5px;
            box-shadow: 0 0 0 1px rgba(255,76,96,.15), 0 0 0 4px rgba(255,76,96,.07);
            margin: 0 0 1.25rem;
          }

          .error-box {
            background: var(--error-box-bg);
            border: 1px solid var(--error-box-border);
            border-radius: var(--radius-md);
            padding: 16px 18px 14px;
            color: var(--error-box-text);
            font-size: .9rem;
            line-height: 1.4;
            margin: 0 0 1.5rem;
            position: relative;
            overflow: auto;
            max-height: 260px;
            transition: background .4s ease, color .25s ease, border-color .4s ease;
          }
          .error-box::-webkit-scrollbar { width: 10px; }
          .error-box::-webkit-scrollbar-track { background: #00000022; border-radius: 10px; }
          .error-box::-webkit-scrollbar-thumb { background: #ff4c60a8; border-radius: 10px; box-shadow: inset 0 0 0 2px #2b1219; }

          table {
            width: 100%;
            border-collapse: collapse;
            margin: 1.25rem 0 0;
            font-size: .83rem;
            background: var(--color-bg-alt);
            border: 1px solid var(--color-border);
            border-radius: var(--radius-md);
            overflow: hidden;
          }
          thead th {
            background: var(--table-head-bg);
            padding: 12px 14px;
            font-size: .78rem;
            letter-spacing: .15ch;
            font-weight: 600;
            text-transform: uppercase;
            color: var(--table-head-text);
            border-bottom: 1px solid var(--color-border);
            transition: background .4s ease, color .25s ease;
          }
          tbody th, tbody td { padding: 9px 12px; border-bottom: 1px solid var(--color-border); }
          tbody tr:last-child th, tbody tr:last-child td { border-bottom: none; }
          tbody tr:nth-child(even) { background: var(--row-alt); }
          tbody tr:hover { background: var(--row-hover); }

          .flag { text-align: center; width: 42px; font-size: 1.05rem; }
          .flag[style] { box-shadow: inset 0 0 0 1px #ff4c60aa; border-radius: var(--radius-sm); transition: background .4s ease; }

          /* Light mode specific overrides */
          body[data-theme='light'] .flag[style] { box-shadow: inset 0 0 0 1px #ef4444cc; }
          body[data-theme='light'] .flag[style*="#361a1f"] { background: #fef2f2 !important; }
          body[data-theme='light'] td[style*='#ff6f7f'] { color: #dc2626 !important; }
          body[data-theme='light'] .error-shell::before,
          body[data-theme='light'] .error-shell::after { display: none; }
          body[data-theme='light'] h3 {
            filter: none;
            background: var(--gradient-accent);
            -webkit-background-clip: text;
            color: transparent;
          }
          body[data-theme='light'] .badge-code {
            background: var(--color-error-soft);
            border-color: #fecaca;
            box-shadow: 0 1px 3px rgba(239, 68, 68, 0.1);
          }
          body[data-theme='light'] .error-shell {
            background: var(--color-bg-alt);
            box-shadow: 0 10px 25px rgba(0,0,0,.04), 0 4px 10px rgba(0,0,0,.06);
          }

          td code.vscode { color: var(--color-accent); font-weight: 500; }

          a { color: var(--color-accent); text-decoration: none; position: relative; }
          a::after { content:''; position:absolute; left:0; bottom:-2px; height:2px; width:100%; background: var(--gradient-accent); opacity:.35; transition:opacity .25s, transform .25s; transform:translateY(3px); }
          a:hover::after { opacity:.9; transform:translateY(0); }

          /* Theme toggle (icon) */
          .theme-toggle { position:absolute; top:14px; right:14px; z-index:5; }
          .theme-toggle button#themeToggleBtn {
            width:32px; height:32px;
            border-radius:50%;
            border:1px solid var(--color-border);
            background: var(--color-bg-alt);
            cursor:pointer;
            display:flex;
            align-items:center;
            justify-content:center;
            font-size:1.0rem;
            color: var(--color-text-dim);
            box-shadow: var(--shadow-sm);
            transition: all .3s ease;
            user-select: none;
          }
          .theme-toggle button#themeToggleBtn:hover {
            color: var(--color-text);
            border-color: var(--color-accent);
            transform: rotate(15deg) scale(1.05);
          }
          .theme-toggle button#themeToggleBtn:active {
            transform: scale(0.95);
          }
          body[data-theme='light'] .theme-toggle button#themeToggleBtn {
            background: rgba(255,255,255,0.9);
            backdrop-filter: blur(8px);
          }

          /* Fast shake animation */
          .error-shell, h3, .badge-code, table, .error-box {
            animation: shakeIn .4s ease-out;
          }
          @keyframes shakeIn {
            0% { opacity: 0; transform: translateX(-8px); }
            25% { opacity: 0.7; transform: translateX(4px); }
            50% { opacity: 0.9; transform: translateX(-2px); }
            75% { opacity: 1; transform: translateX(1px); }
            100% { opacity: 1; transform: translateX(0); }
          }
        """)
            ])
          ]),
          $Body(
            attrs: {'style': 'max-width: 100%;', 'data-theme': 'dark'},
            children: [
              $Div(classes: [
                'error-shell'
              ], children: [
                $Div(classes: [
                  'theme-toggle'
                ], children: [
                  $Button(attrs: {
                    'id': 'themeToggleBtn',
                    'type': 'button',
                    'title': 'Toggle theme',
                    'aria-label': 'Switch between dark and light theme'
                  }, children: [
                    $Text('🌙')
                  ]),
                ]),
                $H3(children: [$Text("< Error $statusCode />")]),
                $P(children: [
                  $Text(
                      "Oops! Something went wrong. Please try again later."),
                ]),
                if (FinchApp.config.isLocalDebug) ...[
                  if (error.isNotEmpty) ...[
                    $Div(
                      attrs: {},
                      children: [
                        $Code(
                          classes: ['error-alert'],
                          children: [$Text(error.toString())],
                        )
                      ],
                    ),
                  ],
                  if (stack.isNotEmpty) ...[
                    $Table(
                      attrs: {},
                      children: [
                        $Thead(children: [
                          $Tr(children: [
                            $Th(
                              attrs: {'colspan': '3'},
                              classes: ['text-align:center'],
                              children: [$Text('Error details')],
                            )
                          ]),
                        ]),
                        $Tbody(
                          children: [
                            for (var stackItem in stack)
                              if (stackItem.toString().isNotEmpty) ...[
                                $Tr(
                                  children: [
                                    $Th(
                                      classes: ['flag'],
                                      attrs: {
                                        if (stackItem
                                            .toString()
                                            .contains('file://'))
                                          'style': 'background-color:#361a1f'
                                      },
                                      children: [
                                        if (stackItem
                                            .toString()
                                            .contains('file://'))
                                          $A(
                                            classes: ['vscode'],
                                            attrs: {
                                              'href': 'javascript:void(0);'
                                            },
                                            children: [
                                              $Text('⛔'),
                                            ],
                                          )
                                        else if (stackItem
                                            .toString()
                                            .contains('package:finch'))
                                          $A(
                                            attrs: {
                                              'target': '_blank',
                                              'title':
                                                  'Report to Finch Developers',
                                              'href':
                                                  'https://github.com/uproid/finch/issues/new?assignees=&labels=bug&template=bug_report.md&title=$statusCode%20Error&body=```\n$stackItem\n```',
                                            },
                                            children: [$Text('🐛')],
                                          )
                                        else
                                          $Text('⚪')
                                      ],
                                    ),
                                    if (stackItem
                                        .toString()
                                        .contains('file://')) ...[
                                      $Td(
                                        attrs: {
                                          'style':
                                              'color:#ff6f7f; font-weight:500;'
                                        },
                                        children: [
                                          $Code(
                                            classes: ['vscode'],
                                            attrs: {
                                              'style':
                                                  'word-break: break-all;'
                                            },
                                            children: [
                                              $Text(stackItem.toString())
                                            ],
                                          ),
                                        ],
                                      )
                                    ] else ...[
                                      $Td(
                                        attrs: {
                                          'style':
                                              'color: var(--color-text-dim);'
                                        },
                                        children: [
                                          $Text(stackItem.toString())
                                        ],
                                      )
                                    ]
                                  ],
                                ),
                              ]
                          ],
                        ),
                      ],
                    ),
                  ],
                ],
                $Script(children: [
                  $Raw("""
              (function(){
                const STORAGE_KEY = 'errorTheme';
                const root = document.body;
                const btn = document.getElementById('themeToggleBtn');

                function applyTheme(theme){
                  root.setAttribute('data-theme', theme);
                  try { localStorage.setItem(STORAGE_KEY, theme); } catch(e) {}
                  if(btn){
                    btn.textContent = theme === 'dark' ? '🌙' : '☀️';
                    btn.title = theme === 'dark' ? 'Switch to light theme' : 'Switch to dark theme';
                  }
                }

                // Load saved theme or default to dark
                const saved = (function(){ try { return localStorage.getItem(STORAGE_KEY);} catch(e){ return null; } })();
                const initialTheme = (saved === 'light' || saved === 'dark') ? saved : (root.getAttribute('data-theme')||'dark');
                applyTheme(initialTheme);

                // Handle toggle click
                if(btn){
                  btn.addEventListener('click', function(){
                    const current = root.getAttribute('data-theme');
                    const next = current === 'dark' ? 'light' : 'dark';
                    applyTheme(next);
                  });
                }

                // Linkify stack vscode paths
                document.querySelectorAll('.vscode').forEach(function(element){
                  var text = element.textContent || '';
                  var regExp = "///([^)*]+)/";
                  var match = text.match(regExp);
                  if(match && match[1]) {
                    element.innerHTML = '<a href="vscode://file/' + match[1] + '">' + text + '</a>';
                  } else {
                    element.textContent = text;
                  }
                });
              })();
            """)
                ])
              ])
            ],
          ),
        ],
      ),
    ],
  );
};