Skip to main content

priv/registry/avatar.json

{
  "files": [
    {
      "content": "defmodule Shadix.Components.Avatar do\n  @moduledoc \"\"\"\n  Avatar, translated from the shadcn/ui (new-york-v4) avatar.\n\n  Radix's Avatar uses an internal image-loading state machine to swap between the\n  image and a fallback. This portable version reproduces that with a tiny hook,\n  `ShadixAvatar` (assets/ts/avatar.ts), placed on the `<img>`: if the image loads\n  it stays visible and the sibling fallback is hidden; if the image errors or has\n  no `src`, the image is hidden and the fallback is revealed.\n\n  Compose them as: `avatar/1` wrapper containing an `avatar_image/1` followed by\n  an `avatar_fallback/1` (the fallback must come after the image so the hook can\n  find it as the next sibling).\n  \"\"\"\n  use Phoenix.Component\n\n  import Shadix.Cn\n\n  @doc \"\"\"\n  Wrapper for an avatar. Renders a `data-slot=\"avatar\"` span; place an\n  `avatar_image/1` and then an `avatar_fallback/1` inside it.\n  \"\"\"\n  attr(:class, :string, default: nil)\n  attr(:rest, :global)\n  slot(:inner_block, required: true)\n\n  def avatar(assigns) do\n    ~H\"\"\"\n    <span\n      data-slot=\"avatar\"\n      class={cn([\"relative flex size-8 shrink-0 overflow-hidden rounded-full\", @class])}\n      {@rest}\n    >\n      {render_slot(@inner_block)}\n    </span>\n    \"\"\"\n  end\n\n  @doc \"\"\"\n  Avatar image. Carries the `ShadixAvatar` hook which hides the image and reveals\n  the sibling fallback on error / missing `src`, and hides the fallback on load.\n  \"\"\"\n  attr(:id, :string, required: true)\n  attr(:src, :string, default: nil)\n  attr(:alt, :string, default: \"\")\n  attr(:class, :string, default: nil)\n  attr(:rest, :global)\n\n  def avatar_image(assigns) do\n    ~H\"\"\"\n    <img\n      data-slot=\"avatar-image\"\n      id={@id}\n      phx-hook=\"ShadixAvatar\"\n      class={cn([\"aspect-square size-full\", @class])}\n      src={@src}\n      alt={@alt}\n      {@rest}\n    />\n    \"\"\"\n  end\n\n  @doc \"\"\"\n  Avatar fallback (typically initials). Starts hidden via the `hidden` class and\n  is revealed by the `ShadixAvatar` hook when the image fails to load.\n  \"\"\"\n  attr(:class, :string, default: nil)\n  attr(:rest, :global)\n  slot(:inner_block, required: true)\n\n  def avatar_fallback(assigns) do\n    ~H\"\"\"\n    <span\n      data-slot=\"avatar-fallback\"\n      class={\n        cn([\n          \"hidden size-full flex items-center justify-center rounded-full bg-muted\",\n          @class\n        ])\n      }\n      {@rest}\n    >\n      {render_slot(@inner_block)}\n    </span>\n    \"\"\"\n  end\nend\n",
      "path": "avatar.ex"
    }
  ],
  "hooks": [
    {
      "content": "interface AvatarHook {\n  el: HTMLImageElement;\n  mounted(): void;\n  destroyed(): void;\n}\n\n// The fallback is the avatar image's next element sibling (an\n// `[data-slot=\"avatar-fallback\"]`). We toggle the Tailwind `hidden` class so the\n// image and fallback are never visible at the same time.\nfunction fallbackOf(el: HTMLElement): HTMLElement | null {\n  let sib = el.nextElementSibling;\n  while (sib) {\n    if (sib.getAttribute(\"data-slot\") === \"avatar-fallback\") {\n      return sib as HTMLElement;\n    }\n    sib = sib.nextElementSibling;\n  }\n  return null;\n}\n\nexport const ShadixAvatar = {\n  mounted(this: AvatarHook) {\n    const img = this.el;\n    const fallback = fallbackOf(img);\n\n    const showFallback = () => {\n      img.classList.add(\"hidden\");\n      fallback?.classList.remove(\"hidden\");\n    };\n    const showImage = () => {\n      img.classList.remove(\"hidden\");\n      fallback?.classList.add(\"hidden\");\n    };\n\n    const src = img.getAttribute(\"src\");\n    if (!src) {\n      showFallback();\n      return;\n    }\n\n    img.addEventListener(\"error\", showFallback);\n    img.addEventListener(\"load\", showImage);\n\n    // The image may have already finished loading (cache) before this hook ran.\n    if (img.complete) {\n      if (img.naturalWidth > 0) showImage();\n      else showFallback();\n    }\n\n    (img as unknown as { _cleanup?: () => void })._cleanup = () => {\n      img.removeEventListener(\"error\", showFallback);\n      img.removeEventListener(\"load\", showImage);\n    };\n  },\n\n  destroyed(this: AvatarHook) {\n    (this.el as unknown as { _cleanup?: () => void })._cleanup?.();\n  },\n};\n",
      "name": "ShadixAvatar",
      "path": "avatar.ts"
    }
  ],
  "name": "avatar",
  "npm_deps": [],
  "registry_deps": [
    "cn"
  ]
}