diff --git a/README.md b/README.md index f383cc2..d63f591 100644 --- a/README.md +++ b/README.md @@ -2,10 +2,10 @@ [![npm version](https://img.shields.io/npm/v/@earthyscience/netcdf4-wasm.svg)](https://www.npmjs.com/package/@earthyscience/netcdf4-wasm) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/EarthyScience/netcdf4-wasm/blob/main/LICENSE) -[![NetCDF4](https://img.shields.io/badge/NetCDF4-Compatible-4B8BBE)](https://www.unidata.ucar.edu/software/netcdf/) +[![NetCDF4](https://img.shields.io/badge/NetCDF4-Compatible-008B8B)](https://www.unidata.ucar.edu/software/netcdf/) [![TypeScript](https://img.shields.io/badge/TypeScript-5.x-3178c6)](https://www.typescriptlang.org/) [![WebAssembly](https://img.shields.io/badge/WebAssembly-654FF0)](https://webassembly.org/) -[![Emscripten](https://img.shields.io/badge/Emscripten-3.x-000000)](https://emscripten.org/) +[![Emscripten](https://img.shields.io/badge/Emscripten-4.0.23-0000)](https://emscripten.org/) [![Jest](https://img.shields.io/badge/Jest-29.x-C21325)](https://jestjs.io/) [![ts-jest](https://img.shields.io/badge/ts--jest-29.x-3178c6)](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" } }