Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions sites/docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
"remark-code-import": "^1.2.0",
"remark-gfm": "^4.0.0",
"rimraf": "^4.4.1",
"runed": "^0.25.0",
"shiki": "^1.2.1",
"svelte": "^5.16.1",
"svelte-check": "^4.1.1",
Expand Down
183 changes: 127 additions & 56 deletions sites/docs/src/lib/components/colors/color-card.svelte
Original file line number Diff line number Diff line change
@@ -1,69 +1,140 @@
<script lang="ts">
import { toast } from "svelte-sonner";
import * as Card from "$lib/registry/ui/card/index.js";
import { colorData } from "$lib/components/colors/color-data.js";
import * as RadioGroup from "$lib/registry/ui/radio-group/index.js";
import { Label } from "$lib/registry/ui/label/index.js";
import { PersistedState } from "runed";
import * as Select from "$lib/registry/ui/select/index.js";
import { AspectRatio } from "$lib/registry/ui/aspect-ratio/index.js";
import { getColors, type ColorPalette } from "$lib/components/colors/colors.js";
import Clipboard from "@lucide/svelte/icons/clipboard";
import Check from "@lucide/svelte/icons/check";
import { scale } from "svelte/transition";

let selectedFormat = "hsl"; // Default color format
type Format = {
format: string;
hint: string;
};

// eslint-disable-next-line @typescript-eslint/no-explicit-any
function handleColorClick(colorName: string, shade: string, colorEntry: any) {
const colorValue = colorEntry[selectedFormat];
toast.success(`Copied "${colorValue}" to clipboard`);
const formats: Format[] = [
{
format: "className",
hint: "bg-slate-100",
},
{
format: "hex",
hint: "#f8fafc",
},
{
format: "rgb",
hint: "248 250 252",
},
{
format: "hsl",
hint: "210 40% 98%",
},
{
format: "oklch",
hint: "0.98 0.00 248",
},
] as const;

navigator.clipboard.writeText(colorValue).catch((err) => {
toast.error(`Failed to copy ${colorName} ${shade} (${colorValue})`);
console.error("Error copying to clipboard:", err);
});
const selectedFormat = new PersistedState("color-format-preference", formats[0].format);

const colors = getColors();

let copied = $state<string>();

async function copy(shade: ColorPalette["colors"][number]) {
copied = shade.className;

const text = shade[selectedFormat.current as never] as string;

await navigator.clipboard.writeText(text);

toast.success(`Copied ${text} to clipboard!`);

setTimeout(() => {
copied = undefined;
}, 1000);
}
</script>

<div class="mb-5 flex flex-col gap-2">
<div class="font-bold">Choose color format to copy:</div>
<RadioGroup.Root bind:value={selectedFormat} class="flex flex-row gap-5">
<div class="flex cursor-pointer items-center space-x-2">
<RadioGroup.Item value="hsl" id="hsl-option" />
<Label for="hsl-option">HSL</Label>
</div>
<div class="flex cursor-pointer items-center space-x-2">
<RadioGroup.Item value="rgb" id="rgb-option" />
<Label for="rgb-option">RGB</Label>
</div>
<div class="flex cursor-pointer items-center space-x-2">
<RadioGroup.Item value="hex" id="hex-option" />
<Label for="hex-option">Hex</Label>
</div>
<div class="flex cursor-pointer items-center space-x-2">
<RadioGroup.Item value="className" id="class-name-option" />
<Label for="class-name-option">Class Name</Label>
</div>
</RadioGroup.Root>
</div>

<Card.Root>
<div class="flex flex-col px-2 pt-5 md:px-5">
{#each Object.entries(colorData) as [colorName, shades] (colorName)}
<div class="">
<h2 class="my-2 capitalize md:text-xl">{colorName}</h2>
<div class="flex w-full flex-col gap-8">
{#each colors as color (color.name)}
<div class="flex w-full flex-col gap-2 rounded-lg border p-2">
<div class="flex place-items-center justify-between">
<h2>{`${color.name[0].toUpperCase()}${color.name.slice(1)}`}</h2>
<Select.Root type="single" bind:value={selectedFormat.current}>
<Select.Trigger class="h-7 w-fit cursor-pointer text-xs">
<span class="mr-2">
<span class="font-bold">Format:</span>
<span class="text-muted-foreground font-mono"
>{selectedFormat.current}</span
>
</span>
</Select.Trigger>
<Select.Content align="end">
{#each formats as format (format.format)}
<Select.Item value={format.format}>
<span>
<span>{format.format}</span>
<span class="text-muted-foreground font-mono"
>{format.hint}</span
>
</span>
</Select.Item>
{/each}
</Select.Content>
</Select.Root>
</div>
<div class="flex flex-col place-items-end md:flex-row md:gap-2">
{#each color.colors as shade (shade.className)}
<button
type="button"
onclick={() => copy(shade)}
class="group w-full flex-1 shrink-0 cursor-pointer md:h-full md:w-auto"
>
<div class="relative">
<div class="hidden md:block">
<AspectRatio ratio={12 / 16}>
<div
class="size-full rounded-lg"
style="background-color: {shade.hex};"
></div>
</AspectRatio>
</div>
<div
class="block h-36 w-full rounded-lg md:hidden"
style="background-color: {shade.hex};"
></div>

<div class="overflox-x-auto mb-5 flex gap-1 md:gap-2">
{#each Object.entries(shades) as [shade, colorEntry] (shade)}
<button
on:click={() => handleColorClick(colorName, shade, colorEntry)}
class="group h-12 w-12 rounded-lg border transition-transform duration-200 sm:h-14 sm:w-14 md:h-20 md:w-20 md:hover:scale-[1.02] lg:h-32 lg:w-32"
style="background-color: {colorEntry.hex};"
>
<div class="h-full w-full" title="{colorName} {shade}"></div>
<div
class="md:group-hover:text-foreground text-foreground/[70%] text-xs sm:text-sm md:text-base"
class="absolute right-2 top-2 opacity-0 transition-all group-hover:opacity-100"
style="color: {shade.foreground};"
>
{shade}
{#if copied === shade.className}
<div in:scale>
<Check class="size-4" />
</div>
{:else}
<div in:scale>
<Clipboard class="size-4" />
</div>
{/if}
</div>
</button>
{/each}
</div>
</div>

<span
class="text-muted-foreground hidden text-nowrap py-1 font-mono text-sm xl:block"
>
{shade.className}
</span>
<span
class="text-muted-foreground block text-nowrap py-1 font-mono text-sm xl:hidden"
>
{shade.className.split("-")[1]}
</span>
</button>
{/each}
</div>
{/each}
</div>
</Card.Root>
</div>
{/each}
</div>
87 changes: 87 additions & 0 deletions sites/docs/src/lib/components/colors/colors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { z } from "zod";

import { colors } from "$lib/registry/colors.js";

const colorSchema = z.object({
name: z.string(),
id: z.string(),
scale: z.number(),
className: z.string(),
hex: z.string(),
rgb: z.string(),
hsl: z.string(),
foreground: z.string(),
oklch: z.string(),
});

const colorPaletteSchema = z.object({
name: z.string(),
colors: z.array(colorSchema),
});

export type ColorPalette = z.infer<typeof colorPaletteSchema>;

export function getColorFormat(color: Color) {
return {
className: `bg-${color.name}-100`,
hex: color.hex,
rgb: color.rgb,
hsl: color.hsl,
oklch: color.oklch,
};
}

export type ColorFormat = keyof ReturnType<typeof getColorFormat>;

export function getColors() {
const tailwindColors = colorPaletteSchema.array().parse(
Object.entries(colors)
.map(([name, color]) => {
if (!Array.isArray(color)) {
return null;
}

return {
name,
colors: color.map((color) => {
const rgb = color.rgb.replace(/^rgb\((\d+),(\d+),(\d+)\)$/, "$1 $2 $3");

return {
...color,
name,
id: `${name}-${color.scale}`,
className: `${name}-${color.scale}`,
rgb,
hsl: color.hsl.replace(
/^hsl\(([\d.]+),([\d.]+%),([\d.]+%)\)$/,
"$1 $2 $3"
),
oklch: color.oklch.replace(
/^oklch\(([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\)$/,
"$1 $2 $3"
),
foreground: getForegroundFromBackground(rgb),
};
}),
};
})
.filter(Boolean)
);

return tailwindColors;
}

export type Color = ReturnType<typeof getColors>[number]["colors"][number];

function getForegroundFromBackground(rgb: string) {
const [r, g, b] = rgb.split(" ").map(Number);

function toLinear(number: number): number {
const base = number / 255;
return base <= 0.04045 ? base / 12.92 : Math.pow((base + 0.055) / 1.055, 2.4);
}

const luminance = 0.2126 * toLinear(r) + 0.7152 * toLinear(g) + 0.0722 * toLinear(b);

return luminance > 0.179 ? "#000" : "#fff";
}
2 changes: 1 addition & 1 deletion sites/docs/src/lib/components/docs/site-footer.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import { siteConfig } from "$lib/config/site.js";
</script>

<footer class="py-6 md:px-8 md:py-0">
<footer class="border-t border-dashed !py-6 md:!py-0 md:px-8">
<div class="container flex flex-col items-center justify-between gap-4 md:h-24 md:flex-row">
<div class="flex flex-col items-center gap-4 px-8 md:flex-row md:gap-2 md:px-0">
<p class="text-muted-foreground text-center text-sm leading-loose md:text-left">
Expand Down
4 changes: 2 additions & 2 deletions sites/docs/src/lib/components/docs/site-header.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@
</script>

<header
class="border-border/40 bg-background/95 supports-[backdrop-filter]:bg-background/60 sticky top-0 z-50 w-full border-b backdrop-blur"
class="bg-background/95 supports-[backdrop-filter]:bg-background/60 sticky top-0 z-50 w-full border-b border-dashed backdrop-blur"
>
<div class="container flex h-14 max-w-screen-2xl items-center">
<div class="container flex h-14 max-w-screen-2xl items-center !pb-0">
<MainNav />
<MobileNav />
<div class="flex flex-1 items-center justify-between space-x-2 md:justify-end">
Expand Down
Loading