build method

  1. @override
View? build()
override

Builds this component's renderable output.

App code can return a node, another component, text, or an iterable of renderable values. The renderer normalizes the value internally.

Implementation

@override
View build() {
  final zoomPercent = (controller.zoom * 100).round();
  final mode = controller.readingMode;

  final (modeLabel, modeIcon) = switch (mode) {
    DocReadingMode.paper => ('Paper', Icons.sun),
    DocReadingMode.dark => ('Dark', Icons.moon),
    DocReadingMode.sepia => ('Sepia', Icons.book),
  };

  return Container(
    className: className,
    dartStyle: dartStyle ??
        DartStyle(
          width: const SizeValue('min(920px, 100%)'),
          margin: const EdgeInsets.only(bottom: 20),
          padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
          radius: 8,
          background: const Color('#131822'),
          border: Border.all(color: const Color.rgba(255, 255, 255, 0.12)),
          display: Display.flex,
          alignItems: AlignItems.center,
          justifyContent: JustifyContent.between,
          gap: 12,
          flexWrap: FlexWrap.wrap,
          shadow: const Shadow(
            y: 8,
            blur: 24,
            spread: -4,
            color: Color.rgba(0, 0, 0, 0.45),
          ),
        ),
    children: [
      // 🔹 Left: Document Title & TOC Drawer Toggle
      Row(
        dartStyle: DartStyle(
            display: Display.flex, alignItems: AlignItems.center, gap: 8),
        children: [
          if (showToc && controller.tableOfContents.isNotEmpty)
            IconButton(
              icon: Icons.list,
              label: 'Table of Contents',
              tooltip: 'Table of Contents',
              variant: controller.showOutline
                  ? ButtonVariant.solid
                  : ButtonVariant.ghost,
              size: ComponentSize.sm,
              onPressed: (_) => controller.toggleOutline(),
            ),
          if (title != null)
            Text.strong(
              title!,
              dartStyle: const DartStyle(
                fontSize: 13,
                fontWeight: 800,
                color: Color('#f8fafc'),
                maxWidth: SizeValue('200px'),
                overflow: 'hidden',
                textOverflow: TextOverflow.ellipsis,
                whiteSpace: 'nowrap',
              ),
            ),
        ],
      ),

      // 🔹 Center: Page Navigation Stepper
      if (showPageNav)
        Row(
          dartStyle: DartStyle(
              display: Display.flex, alignItems: AlignItems.center, gap: 4),
          children: [
            IconButton(
              icon: Icons.chevronLeft,
              label: 'Previous Page',
              size: ComponentSize.sm,
              disabled: controller.currentPage <= 1,
              onPressed: (_) => controller.prevPage(),
            ),
            Container(
              dartStyle: DartStyle(
                padding:
                    const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
                background: const Color.rgba(255, 255, 255, 0.06),
                border:
                    Border.all(color: const Color.rgba(255, 255, 255, 0.1)),
                radius: 4,
                fontSize: 12,
                fontWeight: 700,
                color: const Color('#cbd5e1'),
              ),
              child: Text.span(
                  'Page ${controller.currentPage} of ${controller.totalPages}'),
            ),
            IconButton(
              icon: Icons.chevronRight,
              label: 'Next Page',
              size: ComponentSize.sm,
              disabled: controller.currentPage >= controller.totalPages,
              onPressed: (_) => controller.nextPage(),
            ),
          ],
        ),

      // 🔹 Right: Reading Mode, Zoom & Live Search Controls
      Row(
        dartStyle: DartStyle(
            display: Display.flex, alignItems: AlignItems.center, gap: 8),
        children: [
          // Reading Mode Switcher
          if (showReadingMode)
            Button(
              size: ComponentSize.sm,
              variant: ButtonVariant.ghost,
              onPressed: (_) => controller.toggleReadingMode(),
              children: [
                Icon(modeIcon,
                    size: 14,
                    color: mode == DocReadingMode.dark
                        ? const Color('#38bdf8')
                        : const Color('#fbbf24')),
                Text.span(modeLabel),
              ],
            ),

          // Zoom Controls
          if (showZoom)
            Row(
              dartStyle: DartStyle(
                  display: Display.flex,
                  alignItems: AlignItems.center,
                  gap: 2),
              children: [
                IconButton(
                  icon: Icons.minus,
                  label: 'Zoom Out',
                  size: ComponentSize.sm,
                  tooltip: 'Zoom Out',
                  onPressed: (_) => controller.zoomOut(),
                ),
                Button(
                  child: Text.span('$zoomPercent%'),
                  size: ComponentSize.sm,
                  variant: ButtonVariant.ghost,
                  onPressed: (_) => controller.resetZoom(),
                ),
                IconButton(
                  icon: Icons.plus,
                  label: 'Zoom In',
                  size: ComponentSize.sm,
                  tooltip: 'Zoom In',
                  onPressed: (_) => controller.zoomIn(),
                ),
              ],
            ),

          // Live Search Input Box
          if (showSearch)
            Container(
              dartStyle: DartStyle(
                display: Display.flex,
                alignItems: AlignItems.center,
                gap: 6,
                background: const Color.rgba(255, 255, 255, 0.05),
                padding:
                    const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
                radius: 6,
                border:
                    Border.all(color: const Color.rgba(255, 255, 255, 0.12)),
              ),
              children: [
                Icon(Icons.search, size: 14, color: const Color('#94a3b8')),
                FlintElement(
                  'input',
                  props: {
                    'type': 'text',
                    'placeholder': 'Find in doc...',
                    'value': controller.searchQuery,
                    'style': {
                      'background': 'transparent',
                      'border': 'none',
                      'outline': 'none',
                      'color': '#f8fafc',
                      'font-size': '12px',
                      'width': '100px',
                    },
                    'onInput': (Object event) {
                      final dynamic evt = event;
                      final dynamic target = evt?.target;
                      if (target != null) {
                        controller.search(target.value?.toString() ?? '');
                      }
                    },
                    'onKeyDown': (Object event) {
                      final dynamic evt = event;
                      if (evt?.key == 'Enter') {
                        if (evt?.shiftKey == true) {
                          controller.prevMatch();
                        } else {
                          controller.nextMatch();
                        }
                      }
                    },
                  },
                ),
                if (controller.totalMatches > 0)
                  Text.span(
                    '${controller.currentMatch}/${controller.totalMatches}',
                    dartStyle: const DartStyle(
                        fontSize: 11,
                        color: Color('#38bdf8'),
                        fontWeight: 700),
                  ),
                if (controller.totalMatches > 0)
                  IconButton(
                    icon: Icons.chevronUp,
                    label: 'Previous Match',
                    size: ComponentSize.xs,
                    onPressed: (_) => controller.prevMatch(),
                  ),
                if (controller.totalMatches > 0)
                  IconButton(
                    icon: Icons.chevronDown,
                    label: 'Next Match',
                    size: ComponentSize.xs,
                    onPressed: (_) => controller.nextMatch(),
                  ),
              ],
            ),
        ],
      ),
    ],
  );
}