mirror of
https://github.com/tailscale/tailscale.git
synced 2025-03-17 02:41:00 +00:00
client/web: small UI cleanups
Updates: * Card component used throughout instead of custom card class * SSH toggle changed to non-editable text/status icon in readonly * Red error text on subnet route input when route post failed Updates #10261 Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
This commit is contained in:
parent
e5e5ebda44
commit
d5d42d0293
@ -12,6 +12,8 @@ import SubnetRouterView from "src/components/views/subnet-router-view"
|
|||||||
import { UpdatingView } from "src/components/views/updating-view"
|
import { UpdatingView } from "src/components/views/updating-view"
|
||||||
import useAuth, { AuthResponse } from "src/hooks/auth"
|
import useAuth, { AuthResponse } from "src/hooks/auth"
|
||||||
import { Feature, featureDescription, NodeData } from "src/types"
|
import { Feature, featureDescription, NodeData } from "src/types"
|
||||||
|
import Card from "src/ui/card"
|
||||||
|
import EmptyState from "src/ui/empty-state"
|
||||||
import LoadingDots from "src/ui/loading-dots"
|
import LoadingDots from "src/ui/loading-dots"
|
||||||
import useSWR from "swr"
|
import useSWR from "swr"
|
||||||
import { Link, Route, Router, Switch, useLocation } from "wouter"
|
import { Link, Route, Router, Switch, useLocation } from "wouter"
|
||||||
@ -64,7 +66,7 @@ function WebClient({
|
|||||||
<FeatureRoute path="/ssh" feature="ssh" node={node}>
|
<FeatureRoute path="/ssh" feature="ssh" node={node}>
|
||||||
<SSHView readonly={!auth.canManageNode} node={node} />
|
<SSHView readonly={!auth.canManageNode} node={node} />
|
||||||
</FeatureRoute>
|
</FeatureRoute>
|
||||||
<Route path="/serve">{/* TODO */}Share local content</Route>
|
{/* <Route path="/serve">Share local content</Route> */}
|
||||||
<FeatureRoute path="/update" feature="auto-update" node={node}>
|
<FeatureRoute path="/update" feature="auto-update" node={node}>
|
||||||
<UpdatingView
|
<UpdatingView
|
||||||
versionInfo={node.ClientVersion}
|
versionInfo={node.ClientVersion}
|
||||||
@ -72,7 +74,9 @@ function WebClient({
|
|||||||
/>
|
/>
|
||||||
</FeatureRoute>
|
</FeatureRoute>
|
||||||
<Route>
|
<Route>
|
||||||
<div className="mt-8 card">Page not found</div>
|
<Card className="mt-8">
|
||||||
|
<EmptyState description="Page not found" />
|
||||||
|
</Card>
|
||||||
</Route>
|
</Route>
|
||||||
</Switch>
|
</Switch>
|
||||||
</Router>
|
</Router>
|
||||||
@ -100,9 +104,13 @@ function FeatureRoute({
|
|||||||
return (
|
return (
|
||||||
<Route path={path}>
|
<Route path={path}>
|
||||||
{!node.Features[feature] ? (
|
{!node.Features[feature] ? (
|
||||||
<div className="mt-8 card">
|
<Card className="mt-8">
|
||||||
{featureDescription(feature)} not available on this device.
|
<EmptyState
|
||||||
</div>
|
description={`${featureDescription(
|
||||||
|
feature
|
||||||
|
)} not available on this device.`}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
) : (
|
) : (
|
||||||
children
|
children
|
||||||
)}
|
)}
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
import React from "react"
|
import React from "react"
|
||||||
import { VersionInfo } from "src/types"
|
import { VersionInfo } from "src/types"
|
||||||
import Button from "src/ui/button"
|
import Button from "src/ui/button"
|
||||||
|
import Card from "src/ui/card"
|
||||||
import { useLocation } from "wouter"
|
import { useLocation } from "wouter"
|
||||||
|
|
||||||
export function UpdateAvailableNotification({
|
export function UpdateAvailableNotification({
|
||||||
@ -14,7 +15,7 @@ export function UpdateAvailableNotification({
|
|||||||
const [, setLocation] = useLocation()
|
const [, setLocation] = useLocation()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="card">
|
<Card>
|
||||||
<h2 className="mb-2">
|
<h2 className="mb-2">
|
||||||
Update available{" "}
|
Update available{" "}
|
||||||
{details.LatestVersion && `(v${details.LatestVersion})`}
|
{details.LatestVersion && `(v${details.LatestVersion})`}
|
||||||
@ -32,7 +33,7 @@ export function UpdateAvailableNotification({
|
|||||||
>
|
>
|
||||||
Update now
|
Update now
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</Card>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,6 +10,7 @@ import NiceIP from "src/components/nice-ip"
|
|||||||
import { UpdateAvailableNotification } from "src/components/update-available"
|
import { UpdateAvailableNotification } from "src/components/update-available"
|
||||||
import { NodeData } from "src/types"
|
import { NodeData } from "src/types"
|
||||||
import Button from "src/ui/button"
|
import Button from "src/ui/button"
|
||||||
|
import Card from "src/ui/card"
|
||||||
import QuickCopy from "src/ui/quick-copy"
|
import QuickCopy from "src/ui/quick-copy"
|
||||||
import { useLocation } from "wouter"
|
import { useLocation } from "wouter"
|
||||||
|
|
||||||
@ -27,7 +28,7 @@ export default function DeviceDetailsView({
|
|||||||
<>
|
<>
|
||||||
<h1 className="mb-10">Device details</h1>
|
<h1 className="mb-10">Device details</h1>
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<div className="-mx-5 card">
|
<Card noPadding className="-mx-5 p-5 details-card">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<h1>{node.DeviceName}</h1>
|
<h1>{node.DeviceName}</h1>
|
||||||
@ -49,14 +50,14 @@ export default function DeviceDetailsView({
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Card>
|
||||||
{node.Features["auto-update"] &&
|
{node.Features["auto-update"] &&
|
||||||
!readonly &&
|
!readonly &&
|
||||||
node.ClientVersion &&
|
node.ClientVersion &&
|
||||||
!node.ClientVersion.RunningLatest && (
|
!node.ClientVersion.RunningLatest && (
|
||||||
<UpdateAvailableNotification details={node.ClientVersion} />
|
<UpdateAvailableNotification details={node.ClientVersion} />
|
||||||
)}
|
)}
|
||||||
<div className="-mx-5 card">
|
<Card noPadding className="-mx-5 p-5 details-card">
|
||||||
<h2 className="mb-2">General</h2>
|
<h2 className="mb-2">General</h2>
|
||||||
<table>
|
<table>
|
||||||
<tbody>
|
<tbody>
|
||||||
@ -109,8 +110,8 @@ export default function DeviceDetailsView({
|
|||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</Card>
|
||||||
<div className="-mx-5 card">
|
<Card noPadding className="-mx-5 p-5 details-card">
|
||||||
<h2 className="mb-2">Addresses</h2>
|
<h2 className="mb-2">Addresses</h2>
|
||||||
<table>
|
<table>
|
||||||
<tbody>
|
<tbody>
|
||||||
@ -160,7 +161,7 @@ export default function DeviceDetailsView({
|
|||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</Card>
|
||||||
<footer className="text-gray-500 text-sm leading-tight text-center">
|
<footer className="text-gray-500 text-sm leading-tight text-center">
|
||||||
<Control.AdminContainer node={node}>
|
<Control.AdminContainer node={node}>
|
||||||
Want even more details? Visit{" "}
|
Want even more details? Visit{" "}
|
||||||
|
@ -9,6 +9,7 @@ import { ReactComponent as Machine } from "src/assets/icons/machine.svg"
|
|||||||
import AddressCard from "src/components/address-copy-card"
|
import AddressCard from "src/components/address-copy-card"
|
||||||
import ExitNodeSelector from "src/components/exit-node-selector"
|
import ExitNodeSelector from "src/components/exit-node-selector"
|
||||||
import { NodeData } from "src/types"
|
import { NodeData } from "src/types"
|
||||||
|
import Card from "src/ui/card"
|
||||||
import { pluralize } from "src/utils/util"
|
import { pluralize } from "src/utils/util"
|
||||||
import { Link, useLocation } from "wouter"
|
import { Link, useLocation } from "wouter"
|
||||||
|
|
||||||
@ -30,14 +31,16 @@ export default function HomeView({
|
|||||||
return (
|
return (
|
||||||
<div className="mb-12 w-full">
|
<div className="mb-12 w-full">
|
||||||
<h2 className="mb-3">This device</h2>
|
<h2 className="mb-3">This device</h2>
|
||||||
<div className="-mx-5 card mb-9">
|
<Card noPadding className="-mx-5 p-5 mb-9">
|
||||||
<div className="flex justify-between items-center text-lg mb-5">
|
<div className="flex justify-between items-center text-lg mb-5">
|
||||||
<Link className="flex items-center" to="/details">
|
<Link className="flex items-center" to="/details">
|
||||||
<div className="w-10 h-10 bg-gray-100 rounded-full justify-center items-center inline-flex">
|
<div className="w-10 h-10 bg-gray-100 rounded-full justify-center items-center inline-flex">
|
||||||
<Machine />
|
<Machine />
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-3">
|
<div className="ml-3">
|
||||||
<h1>{node.DeviceName}</h1>
|
<div className="text-gray-800 text-lg font-medium leading-snug">
|
||||||
|
{node.DeviceName}
|
||||||
|
</div>
|
||||||
<p className="text-gray-500 text-sm leading-[18.20px] flex items-center gap-2">
|
<p className="text-gray-500 text-sm leading-[18.20px] flex items-center gap-2">
|
||||||
<span
|
<span
|
||||||
className={cx("w-2 h-2 inline-block rounded-full", {
|
className={cx("w-2 h-2 inline-block rounded-full", {
|
||||||
@ -69,7 +72,7 @@ export default function HomeView({
|
|||||||
>
|
>
|
||||||
View device details →
|
View device details →
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</Card>
|
||||||
<h2 className="mb-3">Settings</h2>
|
<h2 className="mb-3">Settings</h2>
|
||||||
<div className="grid gap-3">
|
<div className="grid gap-3">
|
||||||
{node.Features["advertise-routes"] && (
|
{node.Features["advertise-routes"] && (
|
||||||
@ -108,9 +111,7 @@ export default function HomeView({
|
|||||||
node.RunningSSHServer
|
node.RunningSSHServer
|
||||||
? {
|
? {
|
||||||
text: "Running",
|
text: "Running",
|
||||||
icon: (
|
icon: <div className="w-2 h-2 bg-green-300 rounded-full" />,
|
||||||
<div className="w-2 h-2 bg-emerald-500 rounded-full" />
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
@ -148,37 +149,36 @@ function SettingsCard({
|
|||||||
const [, setLocation] = useLocation()
|
const [, setLocation] = useLocation()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button onClick={() => setLocation(link)}>
|
||||||
className={cx("-mx-5 card cursor-pointer", { "pb-4": footer }, className)}
|
<Card noPadding className={cx("-mx-5 p-5", className)}>
|
||||||
onClick={() => setLocation(link)}
|
<div className="flex justify-between items-center">
|
||||||
>
|
<div>
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex gap-2">
|
||||||
<div>
|
<p className="text-gray-800 font-medium leading-tight mb-2">
|
||||||
<div className="flex gap-2">
|
{title}
|
||||||
<p className="text-gray-800 font-medium leading-tight mb-2">
|
</p>
|
||||||
{title}
|
{badge && (
|
||||||
</p>
|
<div className="h-5 px-2 bg-gray-100 rounded-full flex items-center gap-2">
|
||||||
{badge && (
|
{badge.icon}
|
||||||
<div className="h-5 px-2 bg-gray-100 rounded-full flex items-center gap-2">
|
<div className="text-gray-500 text-xs font-medium">
|
||||||
{badge.icon}
|
{badge.text}
|
||||||
<div className="text-gray-500 text-xs font-medium">
|
</div>
|
||||||
{badge.text}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
</div>
|
||||||
|
<p className="text-gray-500 text-sm leading-tight">{body}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<ArrowRight className="ml-3" />
|
||||||
</div>
|
</div>
|
||||||
<p className="text-gray-500 text-sm leading-tight">{body}</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
{footer && (
|
||||||
<ArrowRight className="ml-3" />
|
<>
|
||||||
</div>
|
<hr className="my-3" />
|
||||||
</div>
|
<div className="text-gray-500 text-sm leading-tight">{footer}</div>
|
||||||
{footer && (
|
</>
|
||||||
<>
|
)}
|
||||||
<hr className="my-3" />
|
</Card>
|
||||||
<div className="text-gray-500 text-sm leading-tight">{footer}</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
// Copyright (c) Tailscale Inc & AUTHORS
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
import cx from "classnames"
|
||||||
import React from "react"
|
import React from "react"
|
||||||
import { useAPI } from "src/api"
|
import { useAPI } from "src/api"
|
||||||
import * as Control from "src/components/control-components"
|
import * as Control from "src/components/control-components"
|
||||||
@ -32,36 +33,49 @@ export default function SSHView({
|
|||||||
Learn more →
|
Learn more →
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
<Card className="-mx-5 p-4">
|
<Card noPadding className="-mx-5 p-5">
|
||||||
<label className="flex gap-3 items-center">
|
{!readonly ? (
|
||||||
<Toggle
|
<label className="flex gap-3 items-center">
|
||||||
checked={node.RunningSSHServer}
|
<Toggle
|
||||||
onChange={() =>
|
checked={node.RunningSSHServer}
|
||||||
api({
|
onChange={() =>
|
||||||
action: "update-prefs",
|
api({
|
||||||
data: {
|
action: "update-prefs",
|
||||||
RunSSHSet: true,
|
data: {
|
||||||
RunSSH: !node.RunningSSHServer,
|
RunSSHSet: true,
|
||||||
},
|
RunSSH: !node.RunningSSHServer,
|
||||||
})
|
},
|
||||||
}
|
})
|
||||||
disabled={readonly}
|
}
|
||||||
/>
|
/>
|
||||||
<div className="text-black text-sm font-medium leading-tight">
|
<div className="text-black text-sm font-medium leading-tight">
|
||||||
Run Tailscale SSH server
|
Run Tailscale SSH server
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
) : (
|
||||||
|
<div className="inline-flex items-center gap-3">
|
||||||
|
<span
|
||||||
|
className={cx("w-2 h-2 rounded-full", {
|
||||||
|
"bg-green-300": node.RunningSSHServer,
|
||||||
|
"bg-gray-300": !node.RunningSSHServer,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
{node.RunningSSHServer ? "Running" : "Not running"}
|
||||||
</div>
|
</div>
|
||||||
</label>
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
<Control.AdminContainer
|
{node.RunningSSHServer && (
|
||||||
className="text-gray-500 text-sm leading-tight mt-3"
|
<Control.AdminContainer
|
||||||
node={node}
|
className="text-gray-500 text-sm leading-tight mt-3"
|
||||||
>
|
node={node}
|
||||||
Remember to make sure that the{" "}
|
>
|
||||||
<Control.AdminLink node={node} path="/acls">
|
Remember to make sure that the{" "}
|
||||||
tailnet policy file
|
<Control.AdminLink node={node} path="/acls">
|
||||||
</Control.AdminLink>{" "}
|
tailnet policy file
|
||||||
allows other devices to SSH into this device.
|
</Control.AdminLink>{" "}
|
||||||
</Control.AdminContainer>
|
allows other devices to SSH into this device.
|
||||||
|
</Control.AdminContainer>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
// Copyright (c) Tailscale Inc & AUTHORS
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
import cx from "classnames"
|
||||||
import React, { useCallback, useMemo, useState } from "react"
|
import React, { useCallback, useMemo, useState } from "react"
|
||||||
import { useAPI } from "src/api"
|
import { useAPI } from "src/api"
|
||||||
import { ReactComponent as CheckCircle } from "src/assets/icons/check-circle.svg"
|
import { ReactComponent as CheckCircle } from "src/assets/icons/check-circle.svg"
|
||||||
@ -21,6 +22,7 @@ export default function SubnetRouterView({
|
|||||||
node: NodeData
|
node: NodeData
|
||||||
}) {
|
}) {
|
||||||
const api = useAPI()
|
const api = useAPI()
|
||||||
|
|
||||||
const [advertisedRoutes, hasRoutes, hasUnapprovedRoutes] = useMemo(() => {
|
const [advertisedRoutes, hasRoutes, hasUnapprovedRoutes] = useMemo(() => {
|
||||||
const routes = node.AdvertisedRoutes || []
|
const routes = node.AdvertisedRoutes || []
|
||||||
return [routes, routes.length > 0, routes.find((r) => !r.Approved)]
|
return [routes, routes.length > 0, routes.find((r) => !r.Approved)]
|
||||||
@ -30,9 +32,11 @@ export default function SubnetRouterView({
|
|||||||
advertisedRoutes.length === 0 && !readonly
|
advertisedRoutes.length === 0 && !readonly
|
||||||
)
|
)
|
||||||
const [inputText, setInputText] = useState<string>("")
|
const [inputText, setInputText] = useState<string>("")
|
||||||
|
const [postError, setPostError] = useState<string>()
|
||||||
|
|
||||||
const resetInput = useCallback(() => {
|
const resetInput = useCallback(() => {
|
||||||
setInputText("")
|
setInputText("")
|
||||||
|
setPostError("")
|
||||||
setInputOpen(false)
|
setInputOpen(false)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
@ -52,7 +56,7 @@ export default function SubnetRouterView({
|
|||||||
</p>
|
</p>
|
||||||
{!readonly &&
|
{!readonly &&
|
||||||
(inputOpen ? (
|
(inputOpen ? (
|
||||||
<div className="-mx-5 card !border-0 shadow-popover">
|
<Card noPadding className="-mx-5 p-5 !border-0 shadow-popover">
|
||||||
<p className="font-medium leading-snug mb-3">
|
<p className="font-medium leading-snug mb-3">
|
||||||
Advertise new routes
|
Advertise new routes
|
||||||
</p>
|
</p>
|
||||||
@ -61,10 +65,19 @@ export default function SubnetRouterView({
|
|||||||
className="text-sm"
|
className="text-sm"
|
||||||
placeholder="192.168.0.0/24"
|
placeholder="192.168.0.0/24"
|
||||||
value={inputText}
|
value={inputText}
|
||||||
onChange={(e) => setInputText(e.target.value)}
|
onChange={(e) => {
|
||||||
|
setPostError("")
|
||||||
|
setInputText(e.target.value)
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<p className="my-2 h-6 text-gray-500 text-sm leading-tight">
|
<p
|
||||||
Add multiple routes by providing a comma-separated list.
|
className={cx("my-2 h-6 text-sm leading-tight", {
|
||||||
|
"text-gray-500": !postError,
|
||||||
|
"text-red-400": postError,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{postError ||
|
||||||
|
"Add multiple routes by providing a comma-separated list."}
|
||||||
</p>
|
</p>
|
||||||
<div className="flex gap-3">
|
<div className="flex gap-3">
|
||||||
<Button
|
<Button
|
||||||
@ -78,15 +91,17 @@ export default function SubnetRouterView({
|
|||||||
.split(",")
|
.split(",")
|
||||||
.map((r) => ({ Route: r, Approved: false })),
|
.map((r) => ({ Route: r, Approved: false })),
|
||||||
],
|
],
|
||||||
}).then(resetInput)
|
})
|
||||||
|
.then(resetInput)
|
||||||
|
.catch((err: Error) => setPostError(err.message))
|
||||||
}
|
}
|
||||||
disabled={!inputText}
|
disabled={!inputText || postError !== ""}
|
||||||
>
|
>
|
||||||
Advertise {hasRoutes && "new "}routes
|
Advertise {hasRoutes && "new "}routes
|
||||||
</Button>
|
</Button>
|
||||||
{hasRoutes && <Button onClick={resetInput}>Cancel</Button>}
|
{hasRoutes && <Button onClick={resetInput}>Cancel</Button>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Card>
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<Button
|
||||||
intent="primary"
|
intent="primary"
|
||||||
@ -99,7 +114,7 @@ export default function SubnetRouterView({
|
|||||||
<div className="-mx-5 mt-10">
|
<div className="-mx-5 mt-10">
|
||||||
{hasRoutes ? (
|
{hasRoutes ? (
|
||||||
<>
|
<>
|
||||||
<div className="px-5 py-3 bg-white rounded-lg border border-gray-200">
|
<Card noPadding className="px-5 py-3">
|
||||||
{advertisedRoutes.map((r) => (
|
{advertisedRoutes.map((r) => (
|
||||||
<div
|
<div
|
||||||
className="flex justify-between items-center pb-2.5 mb-2.5 border-b border-b-gray-200 last:pb-0 last:mb-0 last:border-b-0"
|
className="flex justify-between items-center pb-2.5 mb-2.5 border-b border-b-gray-200 last:pb-0 last:mb-0 last:border-b-0"
|
||||||
@ -141,7 +156,7 @@ export default function SubnetRouterView({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</Card>
|
||||||
{hasUnapprovedRoutes && (
|
{hasUnapprovedRoutes && (
|
||||||
<Control.AdminContainer
|
<Control.AdminContainer
|
||||||
className="mt-3 w-full text-center text-gray-500 text-sm leading-tight"
|
className="mt-3 w-full text-center text-gray-500 text-sm leading-tight"
|
||||||
|
@ -166,28 +166,25 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@layer components {
|
@layer components {
|
||||||
.card {
|
.details-card h1 {
|
||||||
@apply p-5 bg-white rounded-lg border border-gray-200;
|
|
||||||
}
|
|
||||||
.card h1 {
|
|
||||||
@apply text-gray-800 text-lg font-medium leading-snug;
|
@apply text-gray-800 text-lg font-medium leading-snug;
|
||||||
}
|
}
|
||||||
.card h2 {
|
.details-card h2 {
|
||||||
@apply text-gray-500 text-xs font-semibold uppercase tracking-wide;
|
@apply text-gray-500 text-xs font-semibold uppercase tracking-wide;
|
||||||
}
|
}
|
||||||
.card table {
|
.details-card table {
|
||||||
@apply w-full;
|
@apply w-full;
|
||||||
}
|
}
|
||||||
.card tbody {
|
.details-card tbody {
|
||||||
@apply flex flex-col gap-2;
|
@apply flex flex-col gap-2;
|
||||||
}
|
}
|
||||||
.card tr {
|
.details-card tr {
|
||||||
@apply grid grid-flow-col grid-cols-3 gap-2;
|
@apply grid grid-flow-col grid-cols-3 gap-2;
|
||||||
}
|
}
|
||||||
.card td:first-child {
|
.details-card td:first-child {
|
||||||
@apply text-gray-500 text-sm leading-tight truncate;
|
@apply text-gray-500 text-sm leading-tight truncate;
|
||||||
}
|
}
|
||||||
.card td:last-child {
|
.details-card td:last-child {
|
||||||
@apply col-span-2 text-gray-800 text-sm leading-tight;
|
@apply col-span-2 text-gray-800 text-sm leading-tight;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user