mirror of
https://github.com/tale/headplane.git
synced 2026-08-28 16:07:07 +00:00
feat: create shared button component
This commit is contained in:
@@ -1,25 +0,0 @@
|
|||||||
import clsx from 'clsx'
|
|
||||||
import { type HTMLProps } from 'react'
|
|
||||||
|
|
||||||
type Properties = HTMLProps<HTMLButtonElement> & {
|
|
||||||
readonly isDestructive?: boolean;
|
|
||||||
readonly isDisabled?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Action(properties: Properties) {
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
{...properties}
|
|
||||||
type='button'
|
|
||||||
className={clsx(
|
|
||||||
properties.className,
|
|
||||||
properties.isDisabled && 'opacity-50 cursor-not-allowed',
|
|
||||||
properties.isDestructive
|
|
||||||
? 'text-red-700 dark:text-red-500'
|
|
||||||
: 'text-blue-700 dark:text-blue-400'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{properties.children}
|
|
||||||
</button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import clsx from 'clsx'
|
||||||
|
import { type ButtonHTMLAttributes, type DetailedHTMLProps } from 'react'
|
||||||
|
|
||||||
|
type Properties = {
|
||||||
|
readonly variant?: 'emphasized' | 'normal' | 'destructive';
|
||||||
|
} & DetailedHTMLProps<ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>
|
||||||
|
|
||||||
|
export default function Action(properties: Properties) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
{...properties}
|
||||||
|
className={clsx(
|
||||||
|
'focus:outline-none focus:ring focus:ring-1',
|
||||||
|
'focus:ring-blue-500 dark:focus:ring-blue-300',
|
||||||
|
properties.className,
|
||||||
|
properties.disabled && 'opacity-50 cursor-not-allowed',
|
||||||
|
properties.variant === 'destructive' ? 'text-red-700 dark:text-red-500' : '',
|
||||||
|
properties.variant === 'emphasized' ? 'rounded-lg px-4 py-2 bg-gray-800 dark:bg-gray-700 text-white' : '',
|
||||||
|
!properties.variant || properties.variant === 'normal' ? 'text-blue-700 dark:text-blue-400' : ''
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{properties.children}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@ import clsx from 'clsx'
|
|||||||
import { type DetailedHTMLProps, type InputHTMLAttributes } from 'react'
|
import { type DetailedHTMLProps, type InputHTMLAttributes } from 'react'
|
||||||
|
|
||||||
type Properties = {
|
type Properties = {
|
||||||
readonly isEmbedded?: boolean;
|
readonly variant?: 'embedded' | 'normal';
|
||||||
} & DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>
|
} & DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>
|
||||||
|
|
||||||
export default function Input(properties: Properties) {
|
export default function Input(properties: Properties) {
|
||||||
@@ -14,11 +14,11 @@ export default function Input(properties: Properties) {
|
|||||||
'border-gray-300 dark:border-zinc-700',
|
'border-gray-300 dark:border-zinc-700',
|
||||||
'focus:outline-none focus:ring',
|
'focus:outline-none focus:ring',
|
||||||
'focus:ring-blue-500 dark:focus:ring-blue-300',
|
'focus:ring-blue-500 dark:focus:ring-blue-300',
|
||||||
properties.isEmbedded ? 'bg-transparent' : 'dark:bg-zinc-800',
|
properties.variant === 'embedded' ? 'bg-transparent' : 'dark:bg-zinc-800',
|
||||||
properties.isEmbedded ? 'p-0' : 'px-2.5 py-1.5',
|
properties.variant === 'embedded' ? 'p-0' : 'px-2.5 py-1.5',
|
||||||
properties.isEmbedded ? 'border-none' : 'border',
|
properties.variant === 'embedded' ? 'border-none' : 'border',
|
||||||
properties.isEmbedded ? 'focus:ring-0' : 'focus:ring-1',
|
properties.variant === 'embedded' ? 'focus:ring-0' : 'focus:ring-1',
|
||||||
properties.isEmbedded ? 'rounded-none' : 'rounded-lg',
|
properties.variant === 'embedded' ? 'rounded-none' : 'rounded-lg',
|
||||||
properties.className
|
properties.className
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import { useFetcher } from '@remix-run/react'
|
|||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
|
|
||||||
import Action from '~/components/Action'
|
import Button from '~/components/Button'
|
||||||
import Input from '~/components/Input'
|
import Input from '~/components/Input'
|
||||||
import TableList from '~/components/TableList'
|
import TableList from '~/components/TableList'
|
||||||
|
|
||||||
@@ -102,7 +102,7 @@ export default function Domains({ baseDomain, searchDomains }: Properties) {
|
|||||||
</SortableContext>
|
</SortableContext>
|
||||||
<TableList.Item key='add-sd'>
|
<TableList.Item key='add-sd'>
|
||||||
<Input
|
<Input
|
||||||
isEmbedded
|
variant='embedded'
|
||||||
type='text'
|
type='text'
|
||||||
className='font-mono text-sm'
|
className='font-mono text-sm'
|
||||||
placeholder='Search Domain'
|
placeholder='Search Domain'
|
||||||
@@ -111,9 +111,9 @@ export default function Domains({ baseDomain, searchDomains }: Properties) {
|
|||||||
setNewDomain(event.target.value)
|
setNewDomain(event.target.value)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Action
|
<Button
|
||||||
className='text-sm'
|
className='text-sm'
|
||||||
isDisabled={newDomain.length === 0}
|
disabled={newDomain.length === 0}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
fetcher.submit({
|
fetcher.submit({
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
@@ -127,7 +127,7 @@ export default function Domains({ baseDomain, searchDomains }: Properties) {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Add
|
Add
|
||||||
</Action>
|
</Button>
|
||||||
</TableList.Item>
|
</TableList.Item>
|
||||||
</TableList>
|
</TableList>
|
||||||
</DndContext>
|
</DndContext>
|
||||||
@@ -177,8 +177,8 @@ function Domain({ domain, id, localDomains, isDrag }: DomainProperties) {
|
|||||||
{domain}
|
{domain}
|
||||||
</p>
|
</p>
|
||||||
{isDrag ? undefined : (
|
{isDrag ? undefined : (
|
||||||
<Action
|
<Button
|
||||||
isDestructive
|
variant='destructive'
|
||||||
className='text-sm'
|
className='text-sm'
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
fetcher.submit({
|
fetcher.submit({
|
||||||
@@ -191,7 +191,7 @@ function Domain({ domain, id, localDomains, isDrag }: DomainProperties) {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Remove
|
Remove
|
||||||
</Action>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,26 +3,27 @@ import { useFetcher } from '@remix-run/react'
|
|||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
|
|
||||||
|
import Button from '~/components/Button'
|
||||||
|
|
||||||
type Properties = {
|
type Properties = {
|
||||||
readonly isEnabled: boolean;
|
readonly isEnabled: boolean;
|
||||||
readonly baseDomain: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Modal({ isEnabled, baseDomain }: Properties) {
|
export default function Modal({ isEnabled }: Properties) {
|
||||||
const [isOpen, setIsOpen] = useState(false)
|
const [isOpen, setIsOpen] = useState(false)
|
||||||
const fetcher = useFetcher()
|
const fetcher = useFetcher()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<button
|
<Button
|
||||||
type='button'
|
variant='emphasized'
|
||||||
className='rounded-lg px-3 py-2 bg-gray-800 text-white w-fit text-sm'
|
className='w-fit text-sm'
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsOpen(true)
|
setIsOpen(true)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{isEnabled ? 'Disable' : 'Enable'} Magic DNS
|
{isEnabled ? 'Disable' : 'Enable'} Magic DNS
|
||||||
</button>
|
</Button>
|
||||||
<Dialog
|
<Dialog
|
||||||
className='relative z-50'
|
className='relative z-50'
|
||||||
open={isOpen} onClose={() => {
|
open={isOpen} onClose={() => {
|
||||||
@@ -39,11 +40,12 @@ export default function Modal({ isEnabled, baseDomain }: Properties) {
|
|||||||
Devices will no longer be accessible via your tailnet domain.
|
Devices will no longer be accessible via your tailnet domain.
|
||||||
The search domain will also be disabled.
|
The search domain will also be disabled.
|
||||||
</Dialog.Description>
|
</Dialog.Description>
|
||||||
<button
|
<Button
|
||||||
|
variant='emphasized'
|
||||||
type='submit'
|
type='submit'
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'rounded-lg py-2 bg-gray-800 text-white w-full mt-12',
|
'w-full mt-12',
|
||||||
isEnabled ? 'bg-red-800' : 'bg-gray-800'
|
isEnabled ? 'bg-red-800 dark:bg-red-500' : ''
|
||||||
)}
|
)}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
fetcher.submit({
|
fetcher.submit({
|
||||||
@@ -58,7 +60,7 @@ export default function Modal({ isEnabled, baseDomain }: Properties) {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{isEnabled ? 'Disable' : 'Enable'}
|
{isEnabled ? 'Disable' : 'Enable'}
|
||||||
</button>
|
</Button>
|
||||||
</Dialog.Panel>
|
</Dialog.Panel>
|
||||||
</div>
|
</div>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { Dialog } from '@headlessui/react'
|
|||||||
import { useFetcher } from '@remix-run/react'
|
import { useFetcher } from '@remix-run/react'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
|
|
||||||
|
import Button from '~/components/Button'
|
||||||
import Code from '~/components/Code'
|
import Code from '~/components/Code'
|
||||||
import Input from '~/components/Input'
|
import Input from '~/components/Input'
|
||||||
|
|
||||||
@@ -32,23 +33,21 @@ export default function Modal({ name }: Properties) {
|
|||||||
<Input
|
<Input
|
||||||
readOnly
|
readOnly
|
||||||
className='font-mono text-sm my-4'
|
className='font-mono text-sm my-4'
|
||||||
// 'my-4 px-3 py-2 border rounded-lg focus:ring-none w-2/3 font-mono text-sm',
|
|
||||||
// 'dark:bg-zinc-800 dark:text-white dark:border-zinc-700'
|
|
||||||
type='text'
|
type='text'
|
||||||
value={name}
|
value={name}
|
||||||
onFocus={event => {
|
onFocus={event => {
|
||||||
event.target.select()
|
event.target.select()
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<button
|
<Button
|
||||||
type='button'
|
variant='emphasized'
|
||||||
className='rounded-lg px-3 py-2 bg-gray-800 text-white w-fit text-sm'
|
className='text-sm w-fit'
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsOpen(true)
|
setIsOpen(true)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Rename Tailnet...
|
Rename Tailnet...
|
||||||
</button>
|
</Button>
|
||||||
<Dialog
|
<Dialog
|
||||||
className='relative z-50'
|
className='relative z-50'
|
||||||
open={isOpen} onClose={() => {
|
open={isOpen} onClose={() => {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { json, useFetcher, useLoaderData } from '@remix-run/react'
|
|||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
|
|
||||||
import Action from '~/components/Action'
|
import Button from '~/components/Button'
|
||||||
import Code from '~/components/Code'
|
import Code from '~/components/Code'
|
||||||
import Input from '~/components/Input'
|
import Input from '~/components/Input'
|
||||||
import TableList from '~/components/TableList'
|
import TableList from '~/components/TableList'
|
||||||
@@ -94,8 +94,8 @@ export default function Page() {
|
|||||||
// eslint-disable-next-line react/no-array-index-key
|
// eslint-disable-next-line react/no-array-index-key
|
||||||
<TableList.Item key={index}>
|
<TableList.Item key={index}>
|
||||||
<p className='font-mono text-sm'>{ns}</p>
|
<p className='font-mono text-sm'>{ns}</p>
|
||||||
<Action
|
<Button
|
||||||
isDestructive
|
variant='destructive'
|
||||||
className='text-sm'
|
className='text-sm'
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
fetcher.submit({
|
fetcher.submit({
|
||||||
@@ -108,12 +108,12 @@ export default function Page() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Remove
|
Remove
|
||||||
</Action>
|
</Button>
|
||||||
</TableList.Item>
|
</TableList.Item>
|
||||||
))}
|
))}
|
||||||
<TableList.Item>
|
<TableList.Item>
|
||||||
<Input
|
<Input
|
||||||
isEmbedded
|
variant='embedded'
|
||||||
type='text'
|
type='text'
|
||||||
className='font-mono text-sm'
|
className='font-mono text-sm'
|
||||||
placeholder='Nameserver'
|
placeholder='Nameserver'
|
||||||
@@ -122,9 +122,9 @@ export default function Page() {
|
|||||||
setNs(event.target.value)
|
setNs(event.target.value)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Action
|
<Button
|
||||||
className='text-sm'
|
className='text-sm'
|
||||||
isDisabled={ns.length === 0}
|
disabled={ns.length === 0}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
fetcher.submit({
|
fetcher.submit({
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
@@ -138,7 +138,7 @@ export default function Page() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Add
|
Add
|
||||||
</Action>
|
</Button>
|
||||||
</TableList.Item>
|
</TableList.Item>
|
||||||
</TableList>
|
</TableList>
|
||||||
{/* TODO: Split DNS and Custom A Records */}
|
{/* TODO: Split DNS and Custom A Records */}
|
||||||
@@ -162,7 +162,7 @@ export default function Page() {
|
|||||||
{' '}
|
{' '}
|
||||||
when Magic DNS is enabled.
|
when Magic DNS is enabled.
|
||||||
</p>
|
</p>
|
||||||
<MagicModal isEnabled={data.magicDns} baseDomain={data.baseDomain}/>
|
<MagicModal isEnabled={data.magicDns}/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
|||||||
export default function Layout() {
|
export default function Layout() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<header className='bg-gray-800 text-white mb-16'>
|
<header className='mb-16 bg-gray-800 text-white dark:bg-gray-700'>
|
||||||
<nav className='container mx-auto'>
|
<nav className='container mx-auto'>
|
||||||
<div className='flex items-center gap-x-2 mb-8 pt-4'>
|
<div className='flex items-center gap-x-2 mb-8 pt-4'>
|
||||||
<CpuChipIcon className='w-8 h-8'/>
|
<CpuChipIcon className='w-8 h-8'/>
|
||||||
|
|||||||
Reference in New Issue
Block a user