# Svelte Widgets — The Custom Canvas

## Conteúdo

- When to Use Svelte (and When Not To)
- The Real Stack
- Dark Mode: Luminance, Not Shadows
- `no_border: true` — Floating Without a Card
- Tabular Numerals on Metrics
- Complete Example: Section Header / Banner
- Widget Skeleton (Quick Start)
- Layout Rules (Avoid PDF Clipping)
- Pre-push Checklist

The Svelte widget is where design goes beyond what native chart types offer.
It is a full Svelte component compiled remotely and rendered inside the dashboard grid —
you own the markup, data fetching, interactivity, and theming.

---

## When to Use Svelte (and When Not To)

**Use Svelte when the native widget types genuinely can't do it:**

- Chart types not in the native palette: sankey, chord, scatter plot, heatmap, treemap, bullet chart.
- Section headers, banners, and callouts — structural layout elements that divide the dashboard.
- Custom metric cards with compound layouts (sparkline + KPI + delta badge in one cell).
- Cross-filter UI controls (segmented buttons, sliders) that drive other widgets.
- Any layout that combines text, icons, and live data in a free-form composition.

**Do NOT use Svelte to reimplement what native widgets already do well.**
A bar chart, a KPI card, a pivot table — native renders faster, behaves consistently with
the grid system, and requires zero Svelte code. Reach for the Svelte canvas only when you
hit a genuine ceiling.

---

## The Real Stack

Everything below comes directly from the scaffold (`widget.Svelte.yaml`). No other
imports or globals are available.

### Charts — Highcharts via `<Chart>`

```svelte
<script>
  import Highcharts from 'highcharts';
  import { Chart } from '@highcharts/svelte';
</script>

<Chart {Highcharts} options={chartOptions} />
```

Always use the `<Chart>` component. Never mount Highcharts manually.

### Tailwind — Markup Theming with `dark:` Variants

Tailwind's `dark:` variants switch automatically when the dashboard theme toggles.
No JavaScript needed for markup-level theming:

```svelte
<div class="bg-white text-gray-900 dark:bg-navy-800 dark:text-gray-100">
```

### Reactive Theme for Charts and Color Logic

Highcharts does not read Tailwind. For chart colors, read the theme reactively:

```svelte
<script>
  export let app;

  // Reactive — reruns when dark/light toggles
  $: isDark = $app?.theme === 'dark';

  // In non-reactive JS (e.g. inside onMount):
  // const isDark = app.get().theme === 'dark';
</script>
```

Canonical color values for chart axes and grids:

```js
const textColor = isDark ? '#aeb8c8' : '#949DAC';
const gridColor = isDark ? '#192132' : '#efefef';
const bgColor   = isDark ? 'transparent' : '#ffffff';
```

### Data Utilities

```js
import _ from 'lodash';      // data processing
import moment from 'moment'; // date parsing (avoids UTC-3 pitfall)
import chroma from 'chroma-js'; // color scales for quantitative data
import * as xlsx from 'xlsx'; // Excel export
```

### Icons — `@iconify/svelte`

```svelte
<script>
  import Icon from '@iconify/svelte';
</script>

<Icon icon="mdi:trending-up" width="20" class="text-indigo-500 dark:text-indigo-400" />
```

Icon color inherits `currentColor` — control it entirely through Tailwind text classes.
Available sets: `mdi`, `lucide`, `tabler`, `ph`. Browse at icon-sets.iconify.design.

---

## Dark Mode: Luminance, Not Shadows

In dark dashboards, box shadows vanish — they rely on light scattering that doesn't exist
against a dark background. Use a **surface elevation ladder** with hairline borders instead:

| Layer      | Light                  | Dark                           |
|------------|------------------------|--------------------------------|
| Canvas     | `bg-gray-50`           | `dark:bg-navy-900`             |
| Card       | `bg-white`             | `dark:bg-navy-800`             |
| Raised     | `bg-white shadow-sm`   | `dark:bg-navy-700`             |
| Separator  | `border-gray-200`      | `dark:border-slate-700`        |

Each step up in elevation is a lighter background, not a deeper shadow.
Add `border border-gray-200 dark:border-slate-700` for hairline separation.

---

## `no_border: true` — Floating Without a Card

By default, widgets render inside a card frame. Set `no_border: true` to remove the
border and background, letting the component float directly on the dashboard canvas:

```yaml
- type: Svelte
  nome: "Section Header"
  no_border: true
  w: 12
  h: 1
  chart:
    components:
      - name: Main.svelte
        code: |
          ...
```

Use `no_border: true` for headers, dividers, banners, and any layout element that
should blend into the grid rather than sit inside a card.

---

## Tabular Numerals on Metrics

Numbers in metric displays must align on the decimal point when stacked.
Apply `font-feature-settings: "tnum"` via Tailwind's `tabular-nums` utility:

```svelte
<span class="font-mono tabular-nums text-2xl font-semibold text-gray-900 dark:text-gray-100">
  {value}
</span>
```

Use `tabular-nums` on every number that appears in a column or next to a label.
Never use proportional numerals in KPI cards — digits shift width and the layout jiggles.

---

## Complete Example: Section Header / Banner

This replaces the structural role of a plain text block. It uses `no_border: true`
to float, Iconify for the icon, and `dark:` variants throughout.

```yaml
- type: Svelte
  nome: "Section Header — Vendas"
  display_nome: false
  w: 12
  h: 1
  x: 0
  y: 0
  mobileY: 0
  mobileH: 1
  mobileVisible: true
  chart:
    components:
      - name: Main.svelte
        code: |
          <script>
            import Icon from '@iconify/svelte';

            export let app;

            // Props you'd customize per header
            const title    = "Vendas & Performance";
            const subtitle = "Período selecionado no filtro de data";
            const icon     = "lucide:bar-chart-2";
          </script>

          <div class="flex items-center gap-3 px-1 py-2 border-b border-gray-200 dark:border-slate-700">
            <span class="flex items-center justify-center w-8 h-8 rounded-lg
                         bg-indigo-50 dark:bg-indigo-900/30">
              <Icon {icon} width="18"
                    class="text-indigo-600 dark:text-indigo-400" />
            </span>

            <div class="flex-1 min-w-0">
              <h2 class="text-sm font-semibold leading-tight
                         text-gray-900 dark:text-gray-100 truncate">
                {title}
              </h2>
              {#if subtitle}
                <p class="text-xs text-gray-400 dark:text-gray-500 truncate">
                  {subtitle}
                </p>
              {/if}
            </div>
          </div>
    filter: []
    drilldownLink: false
    drilldownColumns: []
    no_border: true
```

Key decisions in this example:
- `display_nome: false` — the widget title is suppressed; the component IS the header.
- `no_border: true` (under the chart block) — floats without a card frame.
- `border-b` with `dark:border-slate-700` — one hairline below, elevation via color only.
- Icon in a tinted pill (`bg-indigo-50 dark:bg-indigo-900/30`) — visible in both themes.
- `h: 1` — a single grid row; headers should be as thin as they are loud.

---

## Widget Skeleton (Quick Start)

For data-fetching widgets, always include loading and error states:

```svelte
<script>
  import { onMount } from 'svelte';
  import { Chart } from '@highcharts/svelte';
  import Highcharts from 'highcharts';

  export let app;

  let loading = true;
  let error = null;
  let chartOptions = null;

  $: isDark = $app?.theme === 'dark';

  onMount(async () => {
    try {
      const data = await app.fetchMatrix({
        columns: ['[Table]."COLUMN":AGP', '[Table]."VALUE":SUM'],
        limit: 50,
      });
      if (!data?.Result?.length) throw new Error('Sem dados');

      // build chartOptions using isDark for colors...
      chartOptions = { /* ... */ };
    } catch (err) {
      error = err.message || String(err);
    } finally {
      loading = false;
    }
  });
</script>

{#if loading}
  <div class="flex items-center justify-center h-full text-gray-400 text-sm gap-2">
    <svg class="animate-spin h-4 w-4" viewBox="0 0 24 24">
      <circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="3"
              fill="none" opacity="0.3"/>
      <path fill="currentColor" d="M4 12a8 8 0 018-8v3a5 5 0 00-5 5H4z"/>
    </svg>
    Carregando...
  </div>
{:else if error}
  <div class="flex items-center justify-center h-full">
    <p class="text-red-500 text-sm">{error}</p>
  </div>
{:else if chartOptions}
  <div class="w-full h-full">
    <Chart {Highcharts} options={chartOptions} />
  </div>
{/if}
```

---

## Layout Rules (Avoid PDF Clipping)

The widget mounts inside a host container that controls width, height, and overflow.

**Never combine `h-full` + `overflow-auto` on the root element.**
That combo creates an inner scrollbar that clips content in PDF exports.

- Root element: `w-full` with natural (auto) height and padding. No `h-full` + scroll.
- Children inside the widget: free to use `h-full` or flex-fill against their parent.
- If the widget uses `autoGrow`: the host provides no fixed height, so `h-full` on a
  chart's parent collapses it. Use an explicit `chart: { height: 320 }` instead.

---

## Pre-push Checklist

- [ ] Every column reference includes an expression suffix (`:AGP`, `:SUM`, etc.)
- [ ] Raw numbers go through `app.formatValue()` before display
- [ ] Loading and error states are implemented
- [ ] Empty `Result` is handled (check `data.Result.length`)
- [ ] Highcharts uses `<Chart>` component, not manual mount
- [ ] Tailwind `dark:` variants on all markup colors
- [ ] `$app.theme` used for chart color logic (not hardcoded)
- [ ] Root element has no `h-full` + `overflow-auto` combo
- [ ] Verified with `lumo app export app:<id>` — PNG looks correct
