Skip to the content

Fuda Cards札

Collectible card frames for images and live 3D: a true thickness, an edge, foil, stacks, wear and a binder, as two web components.

Fuda is the Japanese word 札 for a card or a tag. About the name

Contents

How-to

Frameworks

Use the card in React, Vue, Svelte and Angular, with the element imported only in the browser, and the whole page in plain HTML, a bundler or an iframe elsewhere.

Both elements are standard custom elements, so any framework that can render a custom tag can show them. Two rules hold everywhere: import the registration module once, in the browser only (the elements extend HTMLElement, which a server does not have), and set a collection's cards as a property, not an attribute. A card's settings are attributes, and cardAttributes(options) writes them from an object, so the same options work in every framework. Each example below is a file in examples/ that pnpm test:usage builds and opens against the package as npm packs it.

React

The component imports the element in an effect, so a server render never reaches it, and spreads the attributes of the options. Declare the tag for TypeScript once.

import { type CardOptions, cardAttributes } from "@johnmorrisdotca/fuda-cards";
import { useEffect } from "react";

export function FudaCard({ picture, ...options }: CardOptions & { picture: string }) {
  useEffect(() => {
    // The element needs a browser, so it is imported here, never at the top of the file: a server render never reaches an effect.
    void import("@johnmorrisdotca/fuda-cards/element");
  }, []);
  return (
    <fuda-card {...cardAttributes(options)}>
      <img src={picture} alt={options.title ?? ""} />
    </fuda-card>
  );
}
import type { DetailedHTMLProps, HTMLAttributes } from "react";

declare module "react" {
  namespace JSX {
    interface IntrinsicElements {
      "fuda-card": DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement> & Record<string, string | undefined>;
    }
  }
}

Vue 3

Tell the compiler that the tag is a custom element (isCustomElement, in the Vue plugin's template.compilerOptions for Vite), and import the element once the component is on the page.

<script setup lang="ts">
import { onMounted } from "vue";

defineProps<{ title: string; picture: string; stack?: number; wear?: string }>();

// The element needs a browser: import it once the component is on the page, never while a server renders it.
onMounted(() => {
  void import("@johnmorrisdotca/fuda-cards/element");
});
</script>

<template>
  <fuda-card :title="title" :stack="stack" :wear="wear"><img :src="picture" :alt="title" /></fuda-card>
</template>
import { createApp, h } from "vue";
import FudaCard from "./FudaCard.vue";

const picture =
  "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='10'%3E%3Crect width='10' height='10' fill='%23335'/%3E%3C/svg%3E";
const app = createApp({ render: () => h(FudaCard, { title: "Explorer One", picture, stack: 10, wear: "near-mint" }) });
app.config.compilerOptions.isCustomElement = (tag) => tag === "fuda-card";
app.mount("#app");

Svelte

onMount never runs on the server, so the import goes there.

<script lang="ts">
import { onMount } from "svelte";

let {
  title,
  picture,
  stack = 1,
  wear = "",
}: { title: string; picture: string; stack?: number; wear?: string } = $props();

// The element needs a browser: onMount never runs on the server.
onMount(() => {
  void import("@johnmorrisdotca/fuda-cards/element");
});
</script>

<fuda-card {title} {stack} {wear}><img src={picture} alt={title} /></fuda-card>

Angular

Add CUSTOM_ELEMENTS_SCHEMA to the component's schemas and load the element in browser code. A property binding on [attr.name] writes the attribute.

import { Component, CUSTOM_ELEMENTS_SCHEMA, Input, type OnInit } from "@angular/core";

@Component({
  selector: "app-fuda-card",
  standalone: true,
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
  template: `<fuda-card [attr.title]="title" [attr.stack]="stack" [attr.wear]="wear"><img [src]="picture" [alt]="title" /></fuda-card>`,
})
export class FudaCardComponent implements OnInit {
  @Input() title = "Explorer One";
  @Input() picture =
    "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='10'%3E%3Crect width='10' height='10' fill='%23335'/%3E%3C/svg%3E";
  @Input() stack = 10;
  @Input() wear = "near-mint";

  ngOnInit(): void {
    // The element needs a browser: ngOnInit also runs when a server renders, so the import waits for one.
    if (typeof window !== "undefined") void import("@johnmorrisdotca/fuda-cards/element");
  }
}

A collection in a framework

The collection takes an array, so set it as a DOM property: in React with a ref and an effect (ref.current.cards = cards), in Vue with :cards.prop="cards", in Svelte with bind:this and a statement that runs when the array changes, and in Angular with the property binding [cards]="cards". Collections lists what an item holds.

Where to go next

The page itself can be plain HTML from a CDN (HTML and CDN), a bundler's (Bundlers) or an iframe of the embed page (Embed in an iframe). TypeScript's types and the server side are in TypeScript and Node.

View sourceEdit on GitHubMade from the source of @johnmorrisdotca/fuda-cards 0.11.0.