feat(ui/product/social): ChannelBadge, PostCard, CampaignCard (8.0.2)
Extract the pure Hanzo Social display components into @hanzo/ui on the @hanzo/gui idiom (Card/XStack/YStack/Text, $-tokens), data + handlers injected via props, reusing the shared StatusTag. Additive: new product/social/* + one barrel line; no existing export changed. Published @hanzo/ui 8.0.2.
This commit is contained in:
+2
-2
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "@hanzo/ui",
|
||||
"version": "8.0.1",
|
||||
"version": "8.0.2",
|
||||
"type": "module",
|
||||
"description": "Hanzo UI \u2014 the one cross-platform component library on @hanzo/gui. The product/app layer (charts, metrics, page headers, status tags, rich empty states, combobox, slide-over, toasts, drag-reorder, labeled field rows, provider/product marks) + the metadata-driven record layer (@hanzo/data: RecordsView, DataTable, board, typed field editors) + the calm dark-first tokens and motion. Presentational, host-agnostic (data/effects injected), clean-room. Web + native (iOS) + desktop.",
|
||||
"description": "Hanzo UI — the one cross-platform component library on @hanzo/gui. The product/app layer (charts, metrics, page headers, status tags, rich empty states, combobox, slide-over, toasts, drag-reorder, labeled field rows, provider/product marks) + the metadata-driven record layer (@hanzo/data: RecordsView, DataTable, board, typed field editors) + the calm dark-first tokens and motion. Presentational, host-agnostic (data/effects injected), clean-room. Web + native (iOS) + desktop.",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./types/index.d.ts",
|
||||
|
||||
@@ -58,3 +58,6 @@ export * from './color'
|
||||
|
||||
// Usage — the shared AI-quota surface (meter bar → provider card → dashboard).
|
||||
export * from './usage'
|
||||
|
||||
// Social — ChannelBadge, PostCard, CampaignCard (extracted from Hanzo Social).
|
||||
export * from './social'
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
'use client'
|
||||
|
||||
/**
|
||||
* CampaignCard — one marketing campaign (name, channel, lifecycle, budget/spend
|
||||
* bar). Pure: all data via props. Reuses the shared StatusTag for the lifecycle
|
||||
* pill (one status vocabulary). Extracted from Hanzo Social.
|
||||
*/
|
||||
import { Card, Text, XStack, YStack } from '@hanzo/gui'
|
||||
import { StatusTag } from '../StatusTag'
|
||||
|
||||
export type Campaign = {
|
||||
id: string
|
||||
name: string
|
||||
channel: string
|
||||
status: string
|
||||
objective?: string
|
||||
/** minor units (cents) */
|
||||
budget: number
|
||||
spend: number
|
||||
}
|
||||
|
||||
const money = (cents: number) => `$${Math.round((cents || 0) / 100).toLocaleString()}`
|
||||
|
||||
export function CampaignCard({ campaign }: { campaign: Campaign }) {
|
||||
const pct =
|
||||
campaign.budget > 0 ? Math.min(100, (campaign.spend / campaign.budget) * 100) : 0
|
||||
return (
|
||||
<Card p="$4" gap="$3" borderWidth={1} borderColor="$borderColor" width="100%">
|
||||
<XStack items="flex-start" justify="space-between" gap="$2">
|
||||
<YStack gap="$1">
|
||||
<Text fontSize="$4" fontWeight="700">
|
||||
{campaign.name}
|
||||
</Text>
|
||||
<Text fontSize="$2" color="$color11">
|
||||
{campaign.channel}
|
||||
</Text>
|
||||
</YStack>
|
||||
<StatusTag status={campaign.status} />
|
||||
</XStack>
|
||||
|
||||
{campaign.objective ? (
|
||||
<Text fontSize="$2" color="$color11">
|
||||
{campaign.objective}
|
||||
</Text>
|
||||
) : null}
|
||||
|
||||
<YStack gap="$1.5">
|
||||
<XStack justify="space-between">
|
||||
<Text fontSize="$1" color="$color11">
|
||||
{money(campaign.spend)} spent
|
||||
</Text>
|
||||
<Text fontSize="$1" color="$color11">
|
||||
{money(campaign.budget)} budget
|
||||
</Text>
|
||||
</XStack>
|
||||
<YStack height={6} rounded="$2" bg="$color3" overflow="hidden">
|
||||
<YStack height={6} rounded="$2" bg="$color9" width={`${pct}%` as never} />
|
||||
</YStack>
|
||||
</YStack>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
'use client'
|
||||
|
||||
/**
|
||||
* ChannelBadge — the ONE way a social network is marked (brand color + glyph +
|
||||
* optional label), on @hanzo/gui primitives so it renders on web, native, and
|
||||
* desktop. Data-free / host-agnostic. Extracted from Hanzo Social (social.hanzo.ai).
|
||||
*/
|
||||
import { Text, XStack, YStack } from '@hanzo/gui'
|
||||
|
||||
export type Channel =
|
||||
| 'x'
|
||||
| 'facebook'
|
||||
| 'instagram'
|
||||
| 'linkedin'
|
||||
| 'tiktok'
|
||||
| 'youtube'
|
||||
| 'threads'
|
||||
|
||||
const META: Record<Channel, { label: string; bg: string; fg: string; mark: string }> = {
|
||||
x: { label: 'X', bg: '#0f0f12', fg: '#e6e6ea', mark: '𝕏' },
|
||||
facebook: { label: 'Facebook', bg: '#1877f2', fg: '#ffffff', mark: 'f' },
|
||||
instagram: { label: 'Instagram', bg: '#e1306c', fg: '#ffffff', mark: 'IG' },
|
||||
linkedin: { label: 'LinkedIn', bg: '#0a66c2', fg: '#ffffff', mark: 'in' },
|
||||
tiktok: { label: 'TikTok', bg: '#111114', fg: '#25f4ee', mark: 'TT' },
|
||||
youtube: { label: 'YouTube', bg: '#ff0000', fg: '#ffffff', mark: '▶' },
|
||||
threads: { label: 'Threads', bg: '#111114', fg: '#e6e6ea', mark: '@' },
|
||||
}
|
||||
|
||||
export function ChannelBadge({
|
||||
channel,
|
||||
showLabel = false,
|
||||
size = 22,
|
||||
}: {
|
||||
channel: Channel
|
||||
showLabel?: boolean
|
||||
size?: number
|
||||
}) {
|
||||
const m = META[channel] ?? META.x
|
||||
return (
|
||||
<XStack items="center" gap="$2">
|
||||
<YStack
|
||||
width={size}
|
||||
height={size}
|
||||
items="center"
|
||||
justify="center"
|
||||
rounded="$2"
|
||||
bg={m.bg as never}
|
||||
>
|
||||
<Text fontSize="$1" fontWeight="800" color={m.fg as never}>
|
||||
{m.mark}
|
||||
</Text>
|
||||
</YStack>
|
||||
{showLabel ? (
|
||||
<Text fontSize="$3" color="$color12">
|
||||
{m.label}
|
||||
</Text>
|
||||
) : null}
|
||||
</XStack>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
'use client'
|
||||
|
||||
/**
|
||||
* PostCard — one social post: channel, lifecycle, body, schedule time, a media
|
||||
* count, and injected edit/delete affordances. Pure (data + handlers via props);
|
||||
* composes ChannelBadge + the shared StatusTag. Extracted from Hanzo Social.
|
||||
*/
|
||||
import { Card, Text, XStack, YStack, Button } from '@hanzo/gui'
|
||||
import { Pencil, Trash2 } from '@hanzogui/lucide-icons-2'
|
||||
import { StatusTag } from '../StatusTag'
|
||||
import { ChannelBadge, type Channel } from './ChannelBadge'
|
||||
|
||||
export type Post = {
|
||||
id: string
|
||||
content: string
|
||||
channel: Channel
|
||||
status: string
|
||||
/** unix seconds; 0/undefined = not scheduled */
|
||||
scheduleAt?: number
|
||||
media?: string[]
|
||||
}
|
||||
|
||||
const fmt = (unix?: number) =>
|
||||
unix
|
||||
? new Date(unix * 1000).toLocaleString(undefined, {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})
|
||||
: ''
|
||||
|
||||
export function PostCard({
|
||||
post,
|
||||
onEdit,
|
||||
onDelete,
|
||||
}: {
|
||||
post: Post
|
||||
onEdit?: (p: Post) => void
|
||||
onDelete?: (p: Post) => void
|
||||
}) {
|
||||
const mediaCount = post.media?.length ?? 0
|
||||
return (
|
||||
<Card p="$3" gap="$2.5" borderWidth={1} borderColor="$borderColor" width="100%">
|
||||
<XStack items="center" justify="space-between" gap="$2">
|
||||
<XStack items="center" gap="$2">
|
||||
<ChannelBadge channel={post.channel} />
|
||||
<StatusTag status={post.status} />
|
||||
</XStack>
|
||||
<XStack gap="$1">
|
||||
{onEdit ? (
|
||||
<Button size="$2" chromeless icon={<Pencil size={15} />} onPress={() => onEdit(post)} />
|
||||
) : null}
|
||||
{onDelete ? (
|
||||
<Button size="$2" chromeless icon={<Trash2 size={15} />} onPress={() => onDelete(post)} />
|
||||
) : null}
|
||||
</XStack>
|
||||
</XStack>
|
||||
|
||||
<Text fontSize="$3" color="$color12">
|
||||
{post.content}
|
||||
</Text>
|
||||
|
||||
<XStack items="center" justify="space-between" gap="$2">
|
||||
{post.scheduleAt ? (
|
||||
<Text fontSize="$1" color="$color11">
|
||||
{fmt(post.scheduleAt)}
|
||||
</Text>
|
||||
) : (
|
||||
<YStack />
|
||||
)}
|
||||
{mediaCount ? (
|
||||
<Text fontSize="$1" color="$color10">
|
||||
{mediaCount} media
|
||||
</Text>
|
||||
) : null}
|
||||
</XStack>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// @hanzo/ui/product/social — pure, host-agnostic social-media components
|
||||
// extracted from Hanzo Social (social.hanzo.ai). Data + handlers injected via
|
||||
// props; built only on @hanzo/gui primitives + the shared StatusTag.
|
||||
export * from './ChannelBadge'
|
||||
export * from './CampaignCard'
|
||||
export * from './PostCard'
|
||||
Reference in New Issue
Block a user