diff --git a/README.md b/README.md
index f383cc2..d63f591 100644
--- a/README.md
+++ b/README.md
@@ -2,10 +2,10 @@
[](https://www.npmjs.com/package/@earthyscience/netcdf4-wasm)
[](https://github.com/EarthyScience/netcdf4-wasm/blob/main/LICENSE)
-[](https://www.unidata.ucar.edu/software/netcdf/)
+[](https://www.unidata.ucar.edu/software/netcdf/)
[](https://www.typescriptlang.org/)
[](https://webassembly.org/)
-[](https://emscripten.org/)
+[](https://emscripten.org/)
[](https://jestjs.io/)
[](https://kulshekhar.github.io/ts-jest/)
@@ -24,6 +24,10 @@
- 🚀 High-performance WASM compilation
- 📝 Complete TypeScript type definitions
+
+> [!TIP]
+> Want to do more? Plot, visualize, and explore your data at [browzarr.io](https://browzarr.io/)
+
## Installation
```bash
diff --git a/docs/next-js/.gitignore b/docs/next-js/.gitignore
index 5ef6a52..132d89d 100644
--- a/docs/next-js/.gitignore
+++ b/docs/next-js/.gitignore
@@ -39,3 +39,4 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts
+*.wasm
\ No newline at end of file
diff --git a/docs/next-js/app/globals.css b/docs/next-js/app/globals.css
index a2dc41e..e802737 100644
--- a/docs/next-js/app/globals.css
+++ b/docs/next-js/app/globals.css
@@ -1,8 +1,130 @@
@import "tailwindcss";
+@import "tw-animate-css";
:root {
- --background: #ffffff;
- --foreground: #171717;
+ --radius: 0.625rem;
+
+ --background: oklch(1 0 0);
+ --foreground: oklch(0.145 0 0);
+
+ --card: oklch(1 0 0);
+ --card-foreground: oklch(0.145 0 0);
+
+ --popover: oklch(1 0 0);
+ --popover-foreground: oklch(0.145 0 0);
+
+ --primary: oklch(0.205 0 0);
+ --primary-foreground: oklch(0.985 0 0);
+
+ --secondary: oklch(0.97 0 0);
+ --secondary-foreground: oklch(0.205 0 0);
+
+ --muted: oklch(0.97 0 0);
+ --muted-foreground: oklch(0.556 0 0);
+
+ --accent: oklch(0.97 0 0);
+ --accent-foreground: oklch(0.205 0 0);
+
+ --destructive: oklch(0.577 0.245 27.325);
+
+ --border: oklch(0.922 0 0);
+ --input: oklch(0.922 0 0);
+ --ring: oklch(0.708 0 0);
+
+ --chart-1: oklch(0.646 0.222 41.116);
+ --chart-2: oklch(0.6 0.118 184.704);
+ --chart-3: oklch(0.398 0.07 227.392);
+ --chart-4: oklch(0.828 0.189 84.429);
+ --chart-5: oklch(0.769 0.188 70.08);
+
+ --sidebar: oklch(0.985 0 0);
+ --sidebar-foreground: oklch(0.145 0 0);
+ --sidebar-primary: oklch(0.205 0 0);
+ --sidebar-primary-foreground: oklch(0.985 0 0);
+ --sidebar-accent: oklch(0.97 0 0);
+ --sidebar-accent-foreground: oklch(0.205 0 0);
+ --sidebar-border: oklch(0.922 0 0);
+ --sidebar-ring: oklch(0.708 0 0);
+}
+
+@media (prefers-color-scheme: dark) {
+ :root {
+ --background: rgb(29, 29, 29);
+ --foreground: oklch(0.985 0 0);
+
+ --card: oklch(0.205 0 0);
+ --card-foreground: oklch(0.985 0 0);
+
+ --popover: oklch(0.205 0 0);
+ --popover-foreground: oklch(0.985 0 0);
+
+ --primary: oklch(0.922 0 0);
+ --primary-foreground: oklch(0.205 0 0);
+
+ --secondary: oklch(0.269 0 0);
+ --secondary-foreground: oklch(0.985 0 0);
+
+ --muted: oklch(0.269 0 0);
+ --muted-foreground: oklch(0.708 0 0);
+
+ --accent: oklch(0.269 0 0);
+ --accent-foreground: oklch(0.985 0 0);
+
+ --destructive: oklch(0.704 0.191 22.216);
+
+ --border: oklch(1 0 0 / 10%);
+ --input: oklch(1 0 0 / 15%);
+ --ring: oklch(0.556 0 0);
+
+ --chart-1: oklch(0.488 0.243 264.376);
+ --chart-2: oklch(0.696 0.17 162.48);
+ --chart-3: oklch(0.769 0.188 70.08);
+ --chart-4: oklch(0.627 0.265 303.9);
+ --chart-5: oklch(0.645 0.246 16.439);
+
+ --sidebar: oklch(0.205 0 0);
+ --sidebar-foreground: oklch(0.985 0 0);
+ --sidebar-primary: oklch(0.488 0.243 264.376);
+ --sidebar-primary-foreground: oklch(0.985 0 0);
+ --sidebar-accent: oklch(0.269 0 0);
+ --sidebar-accent-foreground: oklch(0.985 0 0);
+ --sidebar-border: oklch(1 0 0 / 10%);
+ --sidebar-ring: oklch(0.556 0 0);
+ }
+}
+
+.dark {
+ --background: rgb(29, 29, 29);
+ --foreground: oklch(0.985 0 0);
+ --card: oklch(0.205 0 0);
+ --card-foreground: oklch(0.985 0 0);
+ --popover: oklch(0.205 0 0);
+ --popover-foreground: oklch(0.985 0 0);
+ --primary: oklch(0.922 0 0);
+ --primary-foreground: oklch(0.205 0 0);
+ --secondary: oklch(0.269 0 0);
+ --secondary-foreground: oklch(0.985 0 0);
+ --muted: oklch(0.269 0 0);
+ --muted-foreground: oklch(0.708 0 0);
+ --accent: oklch(0.269 0 0);
+ --accent-foreground: oklch(0.985 0 0);
+ --destructive: oklch(0.704 0.191 22.216);
+ --border: oklch(1 0 0 / 10%);
+ --input: oklch(1 0 0 / 15%);
+ --ring: oklch(0.556 0 0);
+ --chart-1: oklch(0.488 0.243 264.376);
+ --chart-2: oklch(0.696 0.17 162.48);
+ --chart-3: oklch(0.769 0.188 70.08);
+ --chart-4: oklch(0.627 0.265 303.9);
+ --chart-5: oklch(0.645 0.246 16.439);
+ --sidebar: oklch(0.205 0 0);
+ --sidebar-foreground: oklch(0.985 0 0);
+ --sidebar-primary: oklch(0.488 0.243 264.376);
+ --sidebar-primary-foreground: oklch(0.985 0 0);
+ --sidebar-accent: oklch(0.269 0 0);
+ --sidebar-accent-foreground: oklch(0.985 0 0);
+ --sidebar-border: oklch(1 0 0 / 10%);
+ --sidebar-ring: oklch(0.556 0 0);
}
@theme inline {
@@ -10,17 +132,43 @@
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
+ --color-card: var(--card);
+ --color-card-foreground: var(--card-foreground);
+ --color-popover: var(--popover);
+ --color-popover-foreground: var(--popover-foreground);
+ --color-primary: var(--primary);
+ --color-primary-foreground: var(--primary-foreground);
+ --color-secondary: var(--secondary);
+ --color-secondary-foreground: var(--secondary-foreground);
+ --color-muted: var(--muted);
+ --color-muted-foreground: var(--muted-foreground);
+ --color-accent: var(--accent);
+ --color-accent-foreground: var(--accent-foreground);
+ --color-destructive: var(--destructive);
+ --color-border: var(--border);
+ --color-input: var(--input);
+ --color-ring: var(--ring);
+ --color-chart-1: var(--chart-1);
+ --color-chart-2: var(--chart-2);
+ --color-chart-3: var(--chart-3);
+ --color-chart-4: var(--chart-4);
+ --color-chart-5: var(--chart-5);
+ --color-sidebar: var(--sidebar);
+ --color-sidebar-foreground: var(--sidebar-foreground);
+ --color-sidebar-primary: var(--sidebar-primary);
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
+ --color-sidebar-accent: var(--sidebar-accent);
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
+ --color-sidebar-border: var(--sidebar-border);
+ --color-sidebar-ring: var(--sidebar-ring);
}
-@media (prefers-color-scheme: dark) {
- :root {
- --background: #0a0a0a;
- --foreground: #ededed;
+@layer base {
+ * {
+ @apply border-border outline-ring/50;
}
-}
-body {
- background: var(--background);
- color: var(--foreground);
- font-family: Arial, Helvetica, sans-serif;
-}
+ body {
+ @apply bg-background text-foreground font-sans;
+ }
+}
\ No newline at end of file
diff --git a/docs/next-js/app/hooks/copy-wasm.mjs b/docs/next-js/app/hooks/copy-wasm.mjs
new file mode 100644
index 0000000..4d6b727
--- /dev/null
+++ b/docs/next-js/app/hooks/copy-wasm.mjs
@@ -0,0 +1,14 @@
+import fs from 'fs';
+import path from 'path';
+
+const src = 'node_modules/@earthyscience/netcdf4-wasm/dist';
+const dest = 'public/';
+
+fs.mkdirSync(dest, { recursive: true });
+
+fs.readdirSync(src)
+ .filter(f => f.endsWith('.wasm'))
+ .forEach(f => {
+ fs.copyFileSync(path.join(src, f), path.join(dest, f));
+ console.log(`✓ ${f}`);
+ });
\ No newline at end of file
diff --git a/docs/next-js/app/page.tsx b/docs/next-js/app/page.tsx
index 56a481e..b236615 100644
--- a/docs/next-js/app/page.tsx
+++ b/docs/next-js/app/page.tsx
@@ -1,4 +1,5 @@
import Image from "next/image";
+import LocalNetCDFMeta from "../components/loading/LocalNetCDFMeta";
export default function Home() {
return (
@@ -23,12 +24,7 @@ export default function Home() {
priority
/>
-
-
-
- Let's get started!
-
-
+
);
diff --git a/docs/next-js/components.json b/docs/next-js/components.json
new file mode 100644
index 0000000..b7b9791
--- /dev/null
+++ b/docs/next-js/components.json
@@ -0,0 +1,22 @@
+{
+ "$schema": "https://ui.shadcn.com/schema.json",
+ "style": "new-york",
+ "rsc": true,
+ "tsx": true,
+ "tailwind": {
+ "config": "",
+ "css": "app/globals.css",
+ "baseColor": "neutral",
+ "cssVariables": true,
+ "prefix": ""
+ },
+ "iconLibrary": "lucide",
+ "aliases": {
+ "components": "@/components",
+ "utils": "@/lib/utils",
+ "ui": "@/components/ui",
+ "lib": "@/lib",
+ "hooks": "@/hooks"
+ },
+ "registries": {}
+}
diff --git a/docs/next-js/components/loading/BrowzarrCTA.tsx b/docs/next-js/components/loading/BrowzarrCTA.tsx
new file mode 100644
index 0000000..b713e3c
--- /dev/null
+++ b/docs/next-js/components/loading/BrowzarrCTA.tsx
@@ -0,0 +1,36 @@
+import React from 'react';
+import Link from 'next/link';
+import { Button } from '@/components/ui/button';
+
+interface BrowzarrCTAProps {
+ message?: string;
+ buttonText?: string;
+ className?: string;
+}
+
+export default function BrowzarrCTA({
+ message = "Reading is just the beginning. Explore your data!",
+ buttonText = "Try browzarr.io",
+ className = ""
+}: BrowzarrCTAProps) {
+ return (
+
+
+ {message}
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/docs/next-js/components/loading/LocalNetCDFMeta.tsx b/docs/next-js/components/loading/LocalNetCDFMeta.tsx
new file mode 100644
index 0000000..51ec255
--- /dev/null
+++ b/docs/next-js/components/loading/LocalNetCDFMeta.tsx
@@ -0,0 +1,72 @@
+'use client';
+
+import React, { ChangeEvent, useState } from 'react';
+import { Input } from '@/components/ui/input';
+import { Label } from '@/components/ui/label';
+import { MetaNetCDFAccordion } from './MetaNetCDFAccordion';
+import { NetCDF4 } from '@earthyscience/netcdf4-wasm';
+import BrowzarrCTA from './BrowzarrCTA';
+const LocalNetCDFMeta = () => {
+ const [variables, setVariables] = useState | null>(null);
+ const [attributes, setAttributes] = useState | null>(null);
+ const [metadata, setMetadata] = useState[] | null>(null);
+
+ const handleFileSelect = async (
+ event: ChangeEvent
+ ) => {
+ const files = event.target.files;
+ if (!files || files.length === 0) return;
+
+ const file = files[0];
+
+ try {
+ const data = await NetCDF4.fromBlobLazy(file);
+
+ const [variables, attrs, metadata] = await Promise.all([
+ data.getVariables(),
+ data.getGlobalAttributes(),
+ data.getFullMetadata(),
+ ]);
+
+ setVariables(variables);
+ setAttributes(attrs);
+ setMetadata(metadata);
+
+ } catch (error) {
+ console.error('Error loading NetCDF file:', error);
+ alert('Failed to load NetCDF file. Check console for details.');
+ }
+ };
+
+ return (
+
+
+
+
+
+ {variables && attributes && metadata && (
+
+ )
+ }
+
+
+
+ );
+};
+
+export default LocalNetCDFMeta;
diff --git a/docs/next-js/components/loading/MetaNetCDFAccordion.tsx b/docs/next-js/components/loading/MetaNetCDFAccordion.tsx
new file mode 100644
index 0000000..7fe109f
--- /dev/null
+++ b/docs/next-js/components/loading/MetaNetCDFAccordion.tsx
@@ -0,0 +1,83 @@
+'use client';
+
+import {
+ Accordion,
+ AccordionContent,
+ AccordionItem,
+ AccordionTrigger,
+} from '@/components/ui/accordion';
+
+type Props = {
+ variables: Record;
+ attributes: Record;
+ metadata: Record[];
+};
+
+function ObjectViewer({ data }: { data: Record }) {
+ return (
+
+ {Object.entries(data).map(([key, value]) => (
+
+ {key}
+
+ {typeof value === 'object'
+ ? JSON.stringify(value, null, 2)
+ : String(value)}
+
+
+ ))}
+
+ );
+}
+
+function ArrayViewer({ data }: { data: Record[] }) {
+ return (
+
+ {data.map((item, index) => (
+
+ Item {index + 1}
+
+
+ ))}
+
+ );
+}
+
+export function MetaNetCDFAccordion({
+ variables,
+ attributes,
+ metadata,
+}: Props) {
+ return (
+
+
+ Variables
+
+
+
+
+
+
+ Global Attributes
+
+
+
+
+
+
+ Full Metadata
+
+
+
+
+
+ );
+}
diff --git a/docs/next-js/components/ui/accordion.tsx b/docs/next-js/components/ui/accordion.tsx
new file mode 100644
index 0000000..4a8cca4
--- /dev/null
+++ b/docs/next-js/components/ui/accordion.tsx
@@ -0,0 +1,66 @@
+"use client"
+
+import * as React from "react"
+import * as AccordionPrimitive from "@radix-ui/react-accordion"
+import { ChevronDownIcon } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+function Accordion({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function AccordionItem({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function AccordionTrigger({
+ className,
+ children,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ svg]:rotate-180",
+ className
+ )}
+ {...props}
+ >
+ {children}
+
+
+
+ )
+}
+
+function AccordionContent({
+ className,
+ children,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ {children}
+
+ )
+}
+
+export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
diff --git a/docs/next-js/components/ui/button.tsx b/docs/next-js/components/ui/button.tsx
new file mode 100644
index 0000000..37a7d4b
--- /dev/null
+++ b/docs/next-js/components/ui/button.tsx
@@ -0,0 +1,62 @@
+import * as React from "react"
+import { Slot } from "@radix-ui/react-slot"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const buttonVariants = cva(
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
+ {
+ variants: {
+ variant: {
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
+ destructive:
+ "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
+ outline:
+ "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
+ secondary:
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80",
+ ghost:
+ "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
+ link: "text-primary underline-offset-4 hover:underline",
+ },
+ size: {
+ default: "h-9 px-4 py-2 has-[>svg]:px-3",
+ sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
+ lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
+ icon: "size-9",
+ "icon-sm": "size-8",
+ "icon-lg": "size-10",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ }
+)
+
+function Button({
+ className,
+ variant = "default",
+ size = "default",
+ asChild = false,
+ ...props
+}: React.ComponentProps<"button"> &
+ VariantProps & {
+ asChild?: boolean
+ }) {
+ const Comp = asChild ? Slot : "button"
+
+ return (
+
+ )
+}
+
+export { Button, buttonVariants }
diff --git a/docs/next-js/components/ui/input.tsx b/docs/next-js/components/ui/input.tsx
new file mode 100644
index 0000000..8916905
--- /dev/null
+++ b/docs/next-js/components/ui/input.tsx
@@ -0,0 +1,21 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+function Input({ className, type, ...props }: React.ComponentProps<"input">) {
+ return (
+
+ )
+}
+
+export { Input }
diff --git a/docs/next-js/components/ui/label.tsx b/docs/next-js/components/ui/label.tsx
new file mode 100644
index 0000000..fb5fbc3
--- /dev/null
+++ b/docs/next-js/components/ui/label.tsx
@@ -0,0 +1,24 @@
+"use client"
+
+import * as React from "react"
+import * as LabelPrimitive from "@radix-ui/react-label"
+
+import { cn } from "@/lib/utils"
+
+function Label({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+export { Label }
diff --git a/docs/next-js/lib/utils.ts b/docs/next-js/lib/utils.ts
new file mode 100644
index 0000000..bd0c391
--- /dev/null
+++ b/docs/next-js/lib/utils.ts
@@ -0,0 +1,6 @@
+import { clsx, type ClassValue } from "clsx"
+import { twMerge } from "tailwind-merge"
+
+export function cn(...inputs: ClassValue[]) {
+ return twMerge(clsx(inputs))
+}
diff --git a/docs/next-js/package.json b/docs/next-js/package.json
index 7bc0768..19452d2 100644
--- a/docs/next-js/package.json
+++ b/docs/next-js/package.json
@@ -3,15 +3,24 @@
"version": "0.1.0",
"private": true,
"scripts": {
+ "postinstall": "node app/hooks/copy-wasm.mjs",
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "eslint"
},
"dependencies": {
+ "@earthyscience/netcdf4-wasm": "^0.1.2",
+ "@radix-ui/react-accordion": "^1.2.12",
+ "@radix-ui/react-label": "^2.1.8",
+ "@radix-ui/react-slot": "^1.2.4",
+ "class-variance-authority": "^0.7.1",
+ "clsx": "^2.1.1",
+ "lucide-react": "^0.562.0",
"next": "16.1.1",
"react": "19.2.3",
- "react-dom": "19.2.3"
+ "react-dom": "19.2.3",
+ "tailwind-merge": "^3.4.0"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
@@ -21,6 +30,7 @@
"eslint": "^9",
"eslint-config-next": "16.1.1",
"tailwindcss": "^4",
+ "tw-animate-css": "^1.4.0",
"typescript": "^5"
}
}