Skip to content

Commit

Permalink
docs: add colors page
Browse files Browse the repository at this point in the history
  • Loading branch information
adrian-ub committed Nov 7, 2024
1 parent 84843e7 commit ace3865
Show file tree
Hide file tree
Showing 7 changed files with 437 additions and 0 deletions.
74 changes: 74 additions & 0 deletions docs/src/components/Color.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
---
import { type Color } from '@/lib/colors'
export interface Props {
color: Color
}
const { color } = Astro.props
---

<button
x-data=`{
color: ${JSON.stringify(color)},
copyNotification: false,
copyToClipboard() {
const copyText = this.color[$store.format];
navigator.clipboard.writeText(copyText);
this.copyNotification = true;
let that = this;
setTimeout(function(){
that.copyNotification = false;
}, 2000);
}
}`
@click="copyToClipboard();"
class="group relative flex aspect-[3/1] w-full flex-1 flex-col gap-2 text-[--text] sm:aspect-[2/3] sm:h-auto sm:w-auto [&>svg]:absolute [&>svg]:right-4 [&>svg]:top-4 [&>svg]:h-3.5 [&>svg]:w-3.5 [&>svg]:opacity-0 [&>svg]:transition-opacity"
style={{ '--bg': `hsl(${color.hsl})`, '--text': color.foreground }}
>
<svg
x-show="!copyNotification"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-clipboard group-hover:opacity-100"
>
<rect width="8" height="4" x="8" y="2" rx="1" ry="1"></rect>
<path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"></path>
</svg>

<svg
x-show="copyNotification"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-check group-hover:opacity-100"
>
<path d="M20 6 9 17l-5-5"></path>
</svg>

<div class="w-full flex-1 rounded-md bg-[--bg] md:rounded-lg"></div>
<div class="flex w-full flex-col items-center justify-center gap-1">
<span
class="hidden font-mono text-xs tabular-nums text-muted-foreground transition-colors group-hover:text-foreground lg:flex"
>
{color.class}
</span>
<span
class="font-mono text-xs tabular-nums text-muted-foreground transition-colors group-hover:text-foreground lg:hidden"
>
{color.scale}
</span>
</div>
</button>
186 changes: 186 additions & 0 deletions docs/src/components/ColorFormatSelector.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
---
import { getColorFormat, type Color } from '@/lib/colors'
import { cn } from '@/lib/utils'
export interface Props {
class?: string
color: Color
}
const { color, class: className } = Astro.props
const formats = getColorFormat(color)
const options = Object.entries(formats).map(([format, title]) => ({ value: format, title, format }))
---

<div
class="relative w-64"
x-data=`{
selectOpen: false,
selectedItem: $store.format,
selectableItems: ${JSON.stringify(options)},
selectableItemActive: null,
selectId: $id('select'),
selectKeydownValue: '',
selectKeydownTimeout: 1000,
selectKeydownClearTimeout: null,
selectDropdownPosition: 'bottom',
selectableItemIsActive(item) {
return this.selectableItemActive == item.value;
},
selectableItemActiveNext(){
let index = this.selectableItems.map(a=>a.value).indexOf(this.selectableItemActive);
if(index < this.selectableItems.length-1){
this.selectableItemActive = this.selectableItems[index+1].value;
this.selectScrollToActiveItem();
}
},
selectableItemActivePrevious(){
let index = this.selectableItems.map(a=>a.value).indexOf(this.selectableItemActive);
if(index > 0){
this.selectableItemActive = this.selectableItems[index-1].value;
this.selectScrollToActiveItem();
}
},
selectScrollToActiveItem(){
if(this.selectableItemActive){
activeElement = document.getElementById(this.selectableItemActive + '-' + this.selectId)
newScrollPos = (activeElement.offsetTop + activeElement.offsetHeight) - this.$refs.selectableItemsList.offsetHeight;
if(newScrollPos > 0){
this.$refs.selectableItemsList.scrollTop=newScrollPos;
} else {
this.$refs.selectableItemsList.scrollTop=0;
}
}
},
selectKeydown(event){
if (event.keyCode >= 65 && event.keyCode <= 90) {
this.selectKeydownValue += event.key;
selectedItemBestMatch = this.selectItemsFindBestMatch();
if(selectedItemBestMatch){
if(this.selectOpen){
this.selectableItemActive = selectedItemBestMatch;
this.selectScrollToActiveItem();
} else {
this.selectedItem = this.selectableItemActive = selectedItemBestMatch;
}
}
if(this.selectKeydownValue != ''){
clearTimeout(this.selectKeydownClearTimeout);
this.selectKeydownClearTimeout = setTimeout(() => {
this.selectKeydownValue = '';
}, this.selectKeydownTimeout);
}
}
},
selectItemsFindBestMatch(){
typedValue = this.selectKeydownValue.toLowerCase();
var bestMatch = null;
var bestMatchIndex = -1;
for (var i = 0; i < this.selectableItems.length; i++) {
var title = this.selectableItems[i].title.toLowerCase();
var index = title.indexOf(typedValue);
if (index > -1 && (bestMatchIndex == -1 || index < bestMatchIndex) && !this.selectableItems[i].disabled) {
bestMatch = this.selectableItems[i].value;
bestMatchIndex = index;
}
}
return bestMatch;
},
selectPositionUpdate(){
selectDropdownBottomPos = this.$refs.selectButton.getBoundingClientRect().top + this.$refs.selectButton.offsetHeight + parseInt(window.getComputedStyle(this.$refs.selectableItemsList).maxHeight);
if(window.innerHeight < selectDropdownBottomPos){
this.selectDropdownPosition = 'top';
} else {
this.selectDropdownPosition = 'bottom';
}
}
}`
x-init="
$watch('selectOpen', function(){
if(!selectedItem){
selectableItemActive=selectableItems[0].value;
} else {
selectableItemActive=selectedItem;
}
setTimeout(function(){
selectScrollToActiveItem();
}, 10);
selectPositionUpdate();
window.addEventListener('resize', (event) => { selectPositionUpdate(); });
});
"
x-effect="selectedItem = $store.format;"
@keydown.escape="if(selectOpen){ selectOpen=false; }"
@keydown.down="if(selectOpen){ selectableItemActiveNext(); } else { selectOpen=true; } event.preventDefault();"
@keydown.up="if(selectOpen){ selectableItemActivePrevious(); } else { selectOpen=true; } event.preventDefault();"
@keydown.enter="$store.format=selectableItemActive; selectOpen=false;"
@keydown="selectKeydown($event);"
>
<button
x-ref="selectButton"
@click="selectOpen=!selectOpen"
class={cn(
'flex items-center justify-between whitespace-nowrap border border-input bg-transparent px-3 py-2 shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1 h-7 w-auto gap-1.5 rounded-lg pr-2 text-xs',
className,
)}
>
<span class="font-medium">Format: </span>
<span class="font-mono text-xs text-muted-foreground" x-text="selectedItem ? selectedItem : 'Select Item'"> </span>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-chevron-down h-4 w-4 opacity-50"
aria-hidden="true"
>
<path d="m6 9 6 6 6-6"></path>
</svg>
</button>

<ul
x-show="selectOpen"
x-ref="selectableItemsList"
@click.away="selectOpen = false"
x-transition:enter="transition ease-out duration-50"
x-transition:enter-start="opacity-0 -translate-y-1"
x-transition:enter-end="opacity-100"
:class="{ 'bottom-0 mb-10' : selectDropdownPosition == 'top', 'top-7 mt-10' : selectDropdownPosition == 'bottom' }"
class="absolute z-50 w-full py-1 mt-1 overflow-auto text-sm bg-popover text-popover-foreground rounded-md shadow-md max-h-56 ring-1 ring-black ring-opacity-5 focus:outline-none"
x-cloak
>
<template x-for="item in selectableItems" :key="item.value">
<li
@click="$store.format=item.value; selectOpen=false; $refs.selectButton.focus();"
:id="item.value + '-' + selectId"
:data-disabled="item.disabled"
:class="{ 'bg-accent text-accent-foreground' : selectableItemIsActive(item), '' : !selectableItemIsActive(item) }"
@mousemove="selectableItemActive=item.value"
class="relative flex w-full cursor-default select-none items-center py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground gap-2 rounded-lg [&>span]:flex [&>span]:items-center [&>span]:gap-2"
>
<svg
x-show="selectedItem==item.value"
class="absolute right-2 flex h-3.5 w-3.5 items-center justify-center"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg
>
<span class="font-medium" x-text="item.format"></span>
<span class="font-mono text-xs text-muted-foreground" x-text="item.title"></span>
</li>
</template>
</ul>
</div>
29 changes: 29 additions & 0 deletions docs/src/components/ColorPalette.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
import type { ColorPalette } from '@/lib/colors'
import Color from './Color.astro'
import ColorFormatSelector from './ColorFormatSelector.astro'
export interface Props {
colorPalette: ColorPalette
}
const { colorPalette } = Astro.props
---

<div class="rounded-lg shadow-sm ring-1 ring-border" x-data>
<div class="flex items-center p-2 pb-0">
<div class="flex-1 pl-1 text-sm font-medium">
<h2 class="capitalize">{colorPalette.name}</h2>
</div>
<ColorFormatSelector class="ml-auto" color={colorPalette.colors[0]} />
</div>
<div class="flex flex-col gap-1 p-2 sm:flex-row sm:gap-2">
{colorPalette.colors.map((color) => <Color {color} />)}
</div>
</div>

<script>
import Alpine from 'alpinejs'

Alpine.store('format', 'hsl')
</script>
4 changes: 4 additions & 0 deletions docs/src/config/docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ export const docsConfig: TDocsConfig = {
title: 'Examples',
href: '/examples/authentication',
},
{
title: 'Colors',
href: '/colors',
},
],
sidebarNav: [
{
Expand Down
87 changes: 87 additions & 0 deletions docs/src/lib/colors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { colors } from '@/registry/registry-colors'

import { z } from 'zod'

const colorSchema = z.object({
name: z.string(),
id: z.string(),
scale: z.number(),
class: z.string(),
hex: z.string(),
rgb: z.string(),
hsl: z.string(),
foreground: 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): { class: string, hex: string, rgb: string, hsl: string } {
return {
class: `bg-${color.name}-100`,
hex: color.hex,
rgb: color.rgb,
hsl: color.hsl,
}
}

export type ColorFormat = keyof ReturnType<typeof getColorFormat>

export function getColors(): ColorPalette[] {
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}`,
class: `${name}-${color.scale}`,
rgb,
hsl: color.hsl.replace(
/^hsl\(([\d.]+),([\d.]+%),([\d.]+%)\)$/,
'$1 $2 $3',
),
foreground: getForegroundFromBackground(rgb),
}
}),
}
})
.filter(Boolean),
)

return tailwindColors
}

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

function toLinear(number: number): number {
const base = number / 255
return base <= 0.04045
? base / 12.92
: ((base + 0.055) / 1.055) ** 2.4
}

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

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

return luminance > 0.179 ? '#000' : '#fff'
}
Loading

0 comments on commit ace3865

Please sign in to comment.