begin new custom chart implementation for js ui

This commit is contained in:
2026-07-16 11:45:17 -04:00
parent 13a60a90a1
commit 550e97aa9b
21 changed files with 1001 additions and 179 deletions

View File

@@ -1188,12 +1188,12 @@ func chartsSection() func() *VNode {
),
),
note("webui.ReactiveChart is a stub, and says so",
"The Solid kit's Chart wraps chart.js: it creates a Chart against a <canvas> 2D context "+
"on mount and pushes new data through an effect. None of that — canvas drawing, a JS "+
"charting library, mount lifecycles — exists in the neutral Go runtime, so the Go port "+
"keeps the props shape for parity and renders an empty canvas. Draw with go-chart "+
"instead, as above. Pretending otherwise would be the one thing this site refuses to do."),
note("webui.Chart is a stub, and says so",
"The Solid kit's Chart draws its own SVG — nice-scale axes, rounded columns, arc slices, "+
"a pointer-driven crosshair — with no charting library at all. That geometry has not "+
"been ported to the neutral Go runtime yet, so the Go port keeps the props shape for "+
"parity and renders only its box. This example draws with go-chart "+
"instead, as above. Pretending the port drew charts would be the one thing this site refuses to do."),
)
}
}

View File

@@ -13,6 +13,46 @@
--------------------------------------------------------------------------- */
@custom-variant dark (&:where(.dark, .dark *));
/* ---------------------------------------------------------------------------
Open Sans — self-hosted (files in wwwroot/fonts). One variable file per subset
carries weights 400700 upright and italic, so the four faces below cover the
whole UI. unicode-range keeps the browser to the one subset a glyph needs, and
font-display: swap paints text in the fallback first rather than blocking on the
download. The same four blocks live in the /js side's frontend/css/style.css so
both front-ends render in one typeface. --font-sans (below) points at it. */
@font-face {
font-family: "Open Sans";
font-style: normal;
font-weight: 400 700;
font-display: swap;
src: url("/fonts/opensans-latin-normal.woff2") format("woff2");
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: "Open Sans";
font-style: normal;
font-weight: 400 700;
font-display: swap;
src: url("/fonts/opensans-latin-ext-normal.woff2") format("woff2");
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
font-family: "Open Sans";
font-style: italic;
font-weight: 400 700;
font-display: swap;
src: url("/fonts/opensans-latin-italic.woff2") format("woff2");
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: "Open Sans";
font-style: italic;
font-weight: 400 700;
font-display: swap;
src: url("/fonts/opensans-latin-ext-italic.woff2") format("woff2");
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* App-side design tokens the webui kit references (Tailwind v4 @theme). Brand
values live with the app; the kit stays generic.
@@ -23,6 +63,12 @@
@theme {
--radius-default: 0.375rem;
/* The UI typeface. --font-sans is what Tailwind's preflight points html at, and
the kit's utilities (font-sans) resolve to, so this one line moves the whole
site onto Open Sans; the fallbacks cover the swap window and any glyph outside
the vendored subsets. */
--font-sans: "Open Sans", ui-sans-serif, system-ui, sans-serif;
/* Navy and red — the flag, muted. kjøl is a Norwegian word and the palette says so.
Both are dark and low-key: the page is mostly prose, code and tables, and the brand's
job is to mark the few things you can act on, not to compete with them for attention.
@@ -123,6 +169,7 @@
html {
background-color: var(--color-surface);
color: var(--color-ink);
font-family: var(--font-sans);
}
/* ---------------------------------------------------------------------------

View File

@@ -9,13 +9,54 @@
What is left is only what is genuinely this app's: the brand.
The values below match the Go/WASM section's css/app.css on purpose — same
Lora, same sky accent — so that crossing between /wasm and /js reads as two
parts of ONE site rather than two demos that happen to share a domain. The
Open Sans, same navy accent — so that crossing between /wasm and /js reads as
two parts of ONE site rather than two demos that happen to share a domain. The
two sections are built by completely different pipelines; they should not look
like it.
--------------------------------------------------------------------------- */
/* Open Sans — self-hosted (files in wwwroot/fonts). One variable file per subset
carries weights 400700 upright and italic. unicode-range keeps the browser to the
one subset a glyph needs; font-display: swap paints the fallback first. The same four
blocks live in the /wasm side's css/app.css so both front-ends share one typeface. */
@font-face {
font-family: "Open Sans";
font-style: normal;
font-weight: 400 700;
font-display: swap;
src: url("/fonts/opensans-latin-normal.woff2") format("woff2");
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: "Open Sans";
font-style: normal;
font-weight: 400 700;
font-display: swap;
src: url("/fonts/opensans-latin-ext-normal.woff2") format("woff2");
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
font-family: "Open Sans";
font-style: italic;
font-weight: 400 700;
font-display: swap;
src: url("/fonts/opensans-latin-italic.woff2") format("woff2");
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: "Open Sans";
font-style: italic;
font-weight: 400 700;
font-display: swap;
src: url("/fonts/opensans-latin-ext-italic.woff2") format("woff2");
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@theme {
/* The UI typeface — see the @font-face blocks above. This one line points the kit's
font-sans utilities and the html rule below onto Open Sans. */
--font-sans: "Open Sans", ui-sans-serif, system-ui, sans-serif;
/* The brand: navy throughout, a muted flag-navy. These are the SAME values the Go/WASM
section's css/app.css sets, and under the same names — so the Layers menu, the
sidebar highlight and the callouts are the same navy on both sides of the site rather

View File

@@ -83,7 +83,7 @@ import { CrmTabGroup, CrmSubTabGroup } from "@ui/CrmTabs";
import { Accordion, SingleAccordion } from "@ui/Accordion";
import { SidebarNav } from "@ui/Sidebar";
import { FuzzyMatch } from "@ui/FuzzyMatch";
import ReactiveChart from "@ui/Chart";
import { Chart, ChartSeries } from "@ui/Chart";
import { ThemeToggle, useTheme } from "@ui/Theme";
import { Demo } from "../layout/Demo.tsx";
@@ -1328,7 +1328,7 @@ const KIT_NAMES = [
"AutoTable", "Accordion", "Alert", "Badge", "ButtonUI", "Calendar", "CellGrid", "FormCombobox",
"DatePicker", "FormInput", "Menu", "Modal", "FormMultiSelect", "Popover", "PrettyTable",
"SegmentedButtons", "FormSignaturePad", "TabGroup", "ThemeToggle", "ToastProvider", "ToggleSwitch",
"Tooltip", "TutorialProvider", "SidebarNav", "ReactiveChart", "FuzzyMatch",
"Tooltip", "TutorialProvider", "SidebarNav", "Chart", "FuzzyMatch",
];
function Search() {
@@ -1364,59 +1364,48 @@ function Search() {
// ---- charts --------------------------------------------------------------
const CHART_DAYS = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
function Charts() {
const [seed, setSeed] = createSignal(0);
const data = () => {
const base = [42, 17, 63, 28, 55, 9, 71];
const values = base.map((v) => (seed() === 0 ? v : Math.max(4, (v * 7 + seed() * 13) % 80)));
return {
labels: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
datasets: [
{
label: "Requests",
data: values,
// A literal, not var(--color-primary): chart.js paints a canvas, and a
// canvas cannot read a CSS variable. So this is the one place in the
// site that names the navy instead of asking for it — and it names the
// DARK theme's lighter navy, which is the only one of the two that
// reads on both a white page and a near-black one.
backgroundColor: "#2b4f80",
borderColor: "#2b4f80",
},
],
};
};
// Two series over the same week. seed() reshuffles them so you can watch the SVG
// move — no teardown, no new instance, just the marks that changed.
const shuffle = (base: number[]) =>
base.map((v) => (seed() === 0 ? v : Math.max(4, (v * 7 + seed() * 13) % 80)));
const requests = (): ChartSeries => ({ name: "Requests", data: shuffle([42, 17, 63, 28, 55, 9, 71]) });
const errors = (): ChartSeries => ({ name: "Errors", data: shuffle([8, 3, 12, 6, 9, 2, 14]) });
const pieData = (): ChartSeries => ({ name: "Traffic", data: shuffle([40, 25, 20, 15, 8]) });
const pieLabels = ["Direct", "Search", "Social", "Email", "Referral"];
return (
<Section id="charts" title="Charts">
<Prose>
<code class="font-mono">@ui/Chart</code> wraps chart.js: it creates a chart against a canvas
on mount and pushes new data through an effect, so changing the data updates the chart rather
than rebuilding the page.
<code class="font-mono">@ui/Chart</code> draws SVG no charting library, nothing vendored, no{" "}
<code class="font-mono">&lt;canvas&gt;</code>. Because the picture is JSX it is already reactive:
change <code class="font-mono">series</code> and Solid re-renders the marks that moved. There is
no <code class="font-mono">update()</code> to call.
</Prose>
<Prose>
This is the one component in the kit that is genuinely different on the other side. The Go
port keeps the props shape for parity and renders an empty canvas canvas drawing, a
JavaScript charting library and mount lifecycles do not exist in the neutral Go runtime. Kjol
Wasm Web draws SVG with go-chart instead, and draws it in the browser, in WebAssembly, exactly
as this one draws to a canvas here. Same place, different library. Neither side pretends
otherwise.
Being SVG buys two things a canvas cannot. The marks name the theme tokens
(<code class="font-mono">var(--color-chart-1)</code> ), so the palette inverts for dark mode with
the rest of the site a canvas paints pixels and cannot read a CSS variable. And every mark is a
real element, so hovering shows a crosshair and one tooltip listing every series at that point.
</Prose>
<Panel title="Bar and doughnut, from the same data">
{/* The wrapper is h-full with maintainAspectRatio:false, so the PARENT must
have an explicit height or the canvas collapses to nothing at all. */}
<div class="grid gap-4 lg:grid-cols-12">
<div class="h-72 lg:col-span-7">
<ReactiveChart
type="bar"
data={data()}
options={{ plugins: { legend: { display: false } } }}
/>
<Panel title="Grouped bars, a smooth area, a two-line series, and a donut from one data set">
<div class="grid gap-6 lg:grid-cols-12">
<div class="lg:col-span-7">
<Chart kind="bar" labels={CHART_DAYS} series={[requests(), errors()]} height={260} />
</div>
<div class="h-72 lg:col-span-5">
<ReactiveChart type="doughnut" data={data()} />
<div class="lg:col-span-5">
<Chart kind="donut" labels={pieLabels} series={[pieData()]} height={260} />
</div>
<div class="lg:col-span-7">
<Chart kind="area" curve="smooth" labels={CHART_DAYS} series={[requests()]} height={220} />
</div>
<div class="lg:col-span-5">
<Chart kind="line" labels={CHART_DAYS} series={[requests(), errors()]} height={220} />
</div>
</div>
<div class="mt-4 flex items-center gap-3">
@@ -1424,17 +1413,18 @@ function Charts() {
New data
</ButtonUI>
<span class="text-xs text-ink-muted">
The chart updates in place it is not torn down and rebuilt.
Every chart updates in place hover any of them for the tooltip.
</span>
</div>
</Panel>
<AlertYellow header="It has to be vendored" class="mt-6">
<code class="font-mono">Chart.tsx</code> imports chart.js at the top level and registers
every controller on module load. An app that uses it must vendor chart.js and{" "}
<code class="font-mono">@kurkle/color</code>, or the bundle does not degrade it fails to
evaluate, and you get an empty page and one line in the console.
</AlertYellow>
<Prose>
One <code class="font-mono">kind</code> prop picks the form {" "}
<code class="font-mono">"line" | "area" | "bar" | "pie" | "donut"</code> and{" "}
<code class="font-mono">stacked</code>, <code class="font-mono">curve</code>,{" "}
<code class="font-mono">palette</code> and <code class="font-mono">valueFormat</code> refine it.
The width is measured from the container, so a chart fills whatever column you give it.
</Prose>
</Section>
);
}

View File

@@ -5,12 +5,8 @@
"//3": "Only the files the bundler actually pins are vendored, not the whole npm packages — 3.5 MB rather than 62 MB of type definitions, CJS builds and documentation. If a subpath import is ever added that reaches outside dist/ or build/, this is the first place it will fail.",
"//4": "chart.js (and its @kurkle/color dependency) are here for the same reason: @ui/Chart imports chart.js at the top level and registers every controller on module load. Its wrapper is h-full with maintainAspectRatio:false, so the PARENT must have an explicit height or the canvas collapses to nothing.",
"entrypoints": {
"pdf-lib": "pdf-lib/dist/pdf-lib.esm.js",
"pdfjs-dist": "pdfjs-dist/build/pdf.mjs",
"chart.js": "chart.js/dist/chart.js",
"@kurkle/color": "@kurkle/color/dist/color.esm.js"
"pdfjs-dist": "pdfjs-dist/build/pdf.mjs"
}
}

View File

@@ -0,0 +1,200 @@
/* cyrillic-ext */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 400 700;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWtE6F15M.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 400 700;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWvU6F15M.woff2) format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 400 700;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWtU6F15M.woff2) format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 400 700;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWuk6F15M.woff2) format('woff2');
unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
}
/* hebrew */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 400 700;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWu06F15M.woff2) format('woff2');
unicode-range: U+0307-0308, U+0590-05FF, U+200C-2010, U+20AA, U+25CC, U+FB1D-FB4F;
}
/* math */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 400 700;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWxU6F15M.woff2) format('woff2');
unicode-range: U+0302-0303, U+0305, U+0307-0308, U+0310, U+0312, U+0315, U+031A, U+0326-0327, U+032C, U+032F-0330, U+0332-0333, U+0338, U+033A, U+0346, U+034D, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6, U+03F0-03F1, U+03F4-03F5, U+2016-2017, U+2034-2038, U+203C, U+2040, U+2043, U+2047, U+2050, U+2057, U+205F, U+2070-2071, U+2074-208E, U+2090-209C, U+20D0-20DC, U+20E1, U+20E5-20EF, U+2100-2112, U+2114-2115, U+2117-2121, U+2123-214F, U+2190, U+2192, U+2194-21AE, U+21B0-21E5, U+21F1-21F2, U+21F4-2211, U+2213-2214, U+2216-22FF, U+2308-230B, U+2310, U+2319, U+231C-2321, U+2336-237A, U+237C, U+2395, U+239B-23B7, U+23D0, U+23DC-23E1, U+2474-2475, U+25AF, U+25B3, U+25B7, U+25BD, U+25C1, U+25CA, U+25CC, U+25FB, U+266D-266F, U+27C0-27FF, U+2900-2AFF, U+2B0E-2B11, U+2B30-2B4C, U+2BFE, U+3030, U+FF5B, U+FF5D, U+1D400-1D7FF, U+1EE00-1EEFF;
}
/* symbols */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 400 700;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqW106F15M.woff2) format('woff2');
unicode-range: U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4, U+2150-218F, U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3, U+2218-2219, U+2299, U+22C4-22C6, U+2300-243F, U+2440-244A, U+2460-24FF, U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, U+29EB, U+2B00-2BFF, U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0, U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F, U+1F000-1F0FF, U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, U+1F378, U+1F37D, U+1F382, U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F444, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA, U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, U+1F507-1F50B, U+1F50D, U+1F512-1F513, U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F, U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, U+1F6C6-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F700-1F7FF, U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8BB, U+1F8C0-1F8C1, U+1F900-1F90B, U+1F93B, U+1F946, U+1F984, U+1F996, U+1F9E9, U+1FA00-1FA6F, U+1FA70-1FA7C, U+1FA80-1FA89, U+1FA8F-1FAC6, U+1FACE-1FADC, U+1FADF-1FAE9, U+1FAF0-1FAF8, U+1FB00-1FBFF;
}
/* vietnamese */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 400 700;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWtk6F15M.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 400 700;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWt06F15M.woff2) format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 400 700;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWuU6F.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic-ext */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 400 700;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSKmu1aB.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 400 700;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSumu1aB.woff2) format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 400 700;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSOmu1aB.woff2) format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 400 700;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSymu1aB.woff2) format('woff2');
unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
}
/* hebrew */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 400 700;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTS2mu1aB.woff2) format('woff2');
unicode-range: U+0307-0308, U+0590-05FF, U+200C-2010, U+20AA, U+25CC, U+FB1D-FB4F;
}
/* math */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 400 700;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTVOmu1aB.woff2) format('woff2');
unicode-range: U+0302-0303, U+0305, U+0307-0308, U+0310, U+0312, U+0315, U+031A, U+0326-0327, U+032C, U+032F-0330, U+0332-0333, U+0338, U+033A, U+0346, U+034D, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6, U+03F0-03F1, U+03F4-03F5, U+2016-2017, U+2034-2038, U+203C, U+2040, U+2043, U+2047, U+2050, U+2057, U+205F, U+2070-2071, U+2074-208E, U+2090-209C, U+20D0-20DC, U+20E1, U+20E5-20EF, U+2100-2112, U+2114-2115, U+2117-2121, U+2123-214F, U+2190, U+2192, U+2194-21AE, U+21B0-21E5, U+21F1-21F2, U+21F4-2211, U+2213-2214, U+2216-22FF, U+2308-230B, U+2310, U+2319, U+231C-2321, U+2336-237A, U+237C, U+2395, U+239B-23B7, U+23D0, U+23DC-23E1, U+2474-2475, U+25AF, U+25B3, U+25B7, U+25BD, U+25C1, U+25CA, U+25CC, U+25FB, U+266D-266F, U+27C0-27FF, U+2900-2AFF, U+2B0E-2B11, U+2B30-2B4C, U+2BFE, U+3030, U+FF5B, U+FF5D, U+1D400-1D7FF, U+1EE00-1EEFF;
}
/* symbols */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 400 700;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTUGmu1aB.woff2) format('woff2');
unicode-range: U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4, U+2150-218F, U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3, U+2218-2219, U+2299, U+22C4-22C6, U+2300-243F, U+2440-244A, U+2460-24FF, U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, U+29EB, U+2B00-2BFF, U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0, U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F, U+1F000-1F0FF, U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, U+1F378, U+1F37D, U+1F382, U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F444, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA, U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, U+1F507-1F50B, U+1F50D, U+1F512-1F513, U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F, U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, U+1F6C6-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F700-1F7FF, U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8BB, U+1F8C0-1F8C1, U+1F900-1F90B, U+1F93B, U+1F946, U+1F984, U+1F996, U+1F9E9, U+1FA00-1FA6F, U+1FA70-1FA7C, U+1FA80-1FA89, U+1FA8F-1FAC6, U+1FACE-1FADC, U+1FADF-1FAE9, U+1FAF0-1FAF8, U+1FB00-1FBFF;
}
/* vietnamese */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 400 700;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSCmu1aB.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 400 700;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSGmu1aB.woff2) format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 400 700;
font-stretch: 100%;
font-display: swap;
src: url(https://fonts.gstatic.com/s/opensans/v44/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTS-muw.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}