dragoman 0.3.2

DOI redirection and content negotiation server
<script>
  import rorSvg from './assets/ror.svg'
  import { _ } from '@sveltia/i18n'
  import { Button } from '$lib/components/ui/button/index.js'

  let {
    entry, org, bibLocale = 'en-US', addedId, index,
    dragId, dropPosition, ondragstart, ondragover, ondragleave, ondrop, ondragend,
    onadd, ondelete,
  } = $props()

  let dragHandleActive = $state(false)

  function capitalize(s) {
    return s ? s.charAt(0).toUpperCase() + s.slice(1) : s
  }

  function rorPath(url) {
    return url?.replace('https://ror.org/', '/') ?? url
  }

  function countryName(code) {
    if (!code) return ''
    try {
      return new Intl.DisplayNames([bibLocale], { type: 'region' }).of(code) || code
    } catch { return code }
  }
</script>

{#if org}
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
  draggable={dragHandleActive}
  onpointerdown={() => { dragHandleActive = false }}
  ondragstart={e => { if (!dragHandleActive) { e.preventDefault(); return } ondragstart?.(e) }}
  ondragover={ondragover}
  ondragleave={ondragleave}
  ondrop={ondrop}
  ondragend={() => { dragHandleActive = false; ondragend?.() }}
  class="flex items-start gap-3 px-5 py-4 bg-background transition-opacity
         {dragId === entry.id ? 'opacity-30' : ''}
         {dropPosition?.id === entry.id && dropPosition.before  ? 'border-t-2 border-primary' : ''}
         {dropPosition?.id === entry.id && !dropPosition.before ? 'border-b-2 border-primary' : ''}"
>
  {#if ondragstart}
    <!-- svelte-ignore a11y_no_static_element_interactions -->
    <div
      onpointerdown={e => { e.stopPropagation(); dragHandleActive = true }}
      class="shrink-0 pt-1 cursor-grab active:cursor-grabbing text-muted-foreground/30 hover:text-muted-foreground/60">
      <svg viewBox="0 0 8 14" fill="currentColor" class="w-2 h-3.5">
        <circle cx="2" cy="2"  r="1.5"/>
        <circle cx="6" cy="2"  r="1.5"/>
        <circle cx="2" cy="7"  r="1.5"/>
        <circle cx="6" cy="7"  r="1.5"/>
        <circle cx="2" cy="12" r="1.5"/>
        <circle cx="6" cy="12" r="1.5"/>
      </svg>
    </div>
  {/if}
  <span class="text-xs text-muted-foreground w-5 shrink-0 pt-0.5 text-right">
    {#if index !== null && index !== undefined}{index + 1}.{/if}
  </span>
  <div class="flex-1 min-w-0 space-y-0.5">
    <p class="text-lg font-semibold leading-snug">
      {org.name}
    </p>
    {#if org.acronym || org.additional_names?.length > 0 || org.established || org.country || org.urls.length > 0}
      <dl class="mt-2 space-y-3 text-sm">
        {#if org.acronym}
          <div>
            <dt class="font-semibold text-foreground">{_('org.acronym')}</dt>
            <dd class="text-muted-foreground">{org.acronym}</dd>
          </div>
        {/if}
        {#if org.additional_names?.length > 0}
          <div>
            <dt class="font-semibold text-foreground">{_('org.additional_names')}</dt>
            <dd class="text-muted-foreground">{org.additional_names.join(' · ')}</dd>
          </div>
        {/if}
        {#if org.established}
          <div>
            <dt class="font-semibold text-foreground">{_('org.established')}</dt>
            <dd class="text-muted-foreground">{org.established}</dd>
          </div>
        {/if}
        {#if org.country}
          <div>
            <dt class="font-semibold text-foreground">{_('entity.country')}</dt>
            <dd class="text-muted-foreground">{countryName(org.country)}</dd>
          </div>
        {/if}
        {#if org.urls.length > 0}
          <div>
            <dt class="font-semibold text-foreground">{_('org.website')}</dt>
            <dd class="text-muted-foreground">{#each org.urls as url, i}{#if i > 0} · {/if}<a href={url} target="_blank" rel="noreferrer" class="hover:underline">{url}</a>{/each}</dd>
          </div>
        {/if}
      </dl>
    {/if}
    {#if org.identifiers.length > 0}
      <p class="text-sm text-muted-foreground/60">
        {#each org.identifiers as id, i}{#if i > 0} · {/if}{id.identifier_type}: {id.identifier}{/each}
      </p>
    {/if}
    {#if org.types.length > 0}
      <div class="flex flex-wrap gap-1 my-1">
        {#each org.types as type}
          <span class="inline-flex items-center rounded-full border border-border px-2.5 py-0.5 text-xs font-medium text-muted-foreground">{capitalize(type)}</span>
        {/each}
      </div>
    {/if}
    <a href={rorPath(org.id)}
      class="text-sm text-primary hover:underline inline-flex items-center gap-1.5 pt-0.5"
    ><img src={rorSvg} width="14" height="14" alt="ROR" class="shrink-0" />{org.id}</a>
  </div>
  <div class="flex flex-col gap-0.5 shrink-0">
    <a href={org.id} target="_blank" rel="noreferrer"
      class="inline-flex items-center justify-center h-7 w-7 rounded-md text-muted-foreground hover:text-foreground hover:bg-accent transition-colors"
      aria-label="Open ROR page"
    >
      <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" class="w-4 h-4">
        <path stroke-linecap="round" stroke-linejoin="round" d="M13.5 6H5.25A2.25 2.25 0 0 0 3 8.25v10.5A2.25 2.25 0 0 0 5.25 21h10.5A2.25 2.25 0 0 0 18 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25" />
      </svg>
    </a>
    {#if onadd}
      <Button type="button" variant="ghost" size="icon"
        onclick={() => onadd(org.id, entry.data)}
        aria-label="Add to saved"
        class="h-7 w-7 text-muted-foreground hover:text-foreground"
      >
        {#if addedId === org.id}
          <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" class="w-4 h-4">
            <path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5" />
          </svg>
        {:else}
          <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" class="w-4 h-4">
            <path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
          </svg>
        {/if}
      </Button>
    {/if}
    {#if org.urls.length > 0}
      <a href={org.urls[0]} target="_blank" rel="noreferrer"
        class="inline-flex items-center justify-center h-7 w-7 rounded-md text-muted-foreground hover:text-foreground hover:bg-accent transition-colors"
        aria-label="Open website"
      >
        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" class="w-4 h-4">
          <path stroke-linecap="round" stroke-linejoin="round" d="M12 21a9.004 9.004 0 0 0 8.716-6.747M12 21a9.004 9.004 0 0 1-8.716-6.747M12 21c2.485 0 4.5-4.03 4.5-9S14.485 3 12 3m0 18c-2.485 0-4.5-4.03-4.5-9S9.515 3 12 3m0 0a8.997 8.997 0 0 1 7.843 4.582M12 3a8.997 8.997 0 0 0-7.843 4.582m15.686 0A11.953 11.953 0 0 1 12 10.5c-2.998 0-5.74-1.1-7.843-2.918m15.686 0A8.959 8.959 0 0 1 21 12c0 .778-.099 1.533-.284 2.253m0 0A17.919 17.919 0 0 1 12 16.5c-3.162 0-6.133-.815-8.716-2.247m0 0A9.015 9.015 0 0 1 3 12c0-1.605.42-3.113 1.157-4.418" />
        </svg>
      </a>
    {/if}
    {#if ondelete}
      <Button type="button" variant="ghost" size="icon"
        onclick={ondelete}
        aria-label="Delete entry"
        class="h-7 w-7 text-muted-foreground hover:text-destructive hover:bg-destructive/10"
      >
        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" class="w-4 h-4">
          <path stroke-linecap="round" stroke-linejoin="round" d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" />
        </svg>
      </Button>
    {/if}
  </div>
</div>
{/if}