@olympusoss/canvas 4.0.0 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +108 -0
- package/package.json +14 -3
- package/src/atoms/avatar/avatar.md +185 -0
- package/src/atoms/avatar/avatar.styles.ts +48 -0
- package/src/atoms/avatar/avatar.tsx +99 -0
- package/src/atoms/badge/badge.md +237 -0
- package/src/atoms/badge/badge.styles.ts +79 -0
- package/src/atoms/badge/badge.tsx +86 -0
- package/src/atoms/breadcrumb/breadcrumb.md +233 -0
- package/src/atoms/breadcrumb/breadcrumb.styles.ts +40 -0
- package/src/atoms/breadcrumb/breadcrumb.tsx +130 -0
- package/src/atoms/button/button.android.tsx +6 -0
- package/src/atoms/button/button.ios.tsx +6 -0
- package/src/atoms/button/button.md +184 -0
- package/src/atoms/button/button.shared.tsx +79 -0
- package/src/atoms/button/button.styles.ts +152 -0
- package/src/atoms/button/button.tsx +6 -0
- package/src/atoms/button-group/button-group.android.tsx +6 -0
- package/src/atoms/button-group/button-group.ios.tsx +6 -0
- package/src/atoms/button-group/button-group.md +120 -0
- package/src/atoms/button-group/button-group.shared.tsx +398 -0
- package/src/atoms/button-group/button-group.styles.ts +483 -0
- package/src/atoms/button-group/button-group.tsx +6 -0
- package/src/atoms/checkbox/checkbox.android.tsx +6 -0
- package/src/atoms/checkbox/checkbox.ios.tsx +6 -0
- package/src/atoms/checkbox/checkbox.md +150 -0
- package/src/atoms/checkbox/checkbox.shared.tsx +103 -0
- package/src/atoms/checkbox/checkbox.styles.ts +106 -0
- package/src/atoms/checkbox/checkbox.tsx +6 -0
- package/src/atoms/combobox/combobox.android.tsx +6 -0
- package/src/atoms/combobox/combobox.ios.tsx +6 -0
- package/src/atoms/combobox/combobox.md +213 -0
- package/src/atoms/combobox/combobox.shared.tsx +160 -0
- package/src/atoms/combobox/combobox.styles.ts +270 -0
- package/src/atoms/combobox/combobox.tsx +6 -0
- package/src/atoms/divider/divider.md +140 -0
- package/src/atoms/divider/divider.styles.ts +35 -0
- package/src/atoms/divider/divider.tsx +67 -0
- package/src/atoms/dropdown/dropdown.android.tsx +6 -0
- package/src/atoms/dropdown/dropdown.ios.tsx +6 -0
- package/src/atoms/dropdown/dropdown.md +221 -0
- package/src/atoms/dropdown/dropdown.shared.tsx +190 -0
- package/src/atoms/dropdown/dropdown.styles.ts +233 -0
- package/src/atoms/dropdown/dropdown.tsx +6 -0
- package/src/atoms/icon/icon.md +131 -0
- package/src/atoms/icon/icon.styles.ts +30 -0
- package/src/atoms/icon/icon.tsx +328 -0
- package/src/atoms/index.ts +24 -0
- package/src/atoms/input/input.android.tsx +6 -0
- package/src/atoms/input/input.ios.tsx +6 -0
- package/src/atoms/input/input.md +118 -0
- package/src/atoms/input/input.shared.tsx +203 -0
- package/src/atoms/input/input.styles.ts +286 -0
- package/src/atoms/input/input.tsx +6 -0
- package/src/atoms/kbd/kbd.md +91 -0
- package/src/atoms/kbd/kbd.styles.ts +33 -0
- package/src/atoms/kbd/kbd.tsx +27 -0
- package/src/atoms/listbox/listbox.md +177 -0
- package/src/atoms/listbox/listbox.styles.ts +60 -0
- package/src/atoms/listbox/listbox.tsx +113 -0
- package/src/atoms/pagination/pagination.android.tsx +6 -0
- package/src/atoms/pagination/pagination.ios.tsx +6 -0
- package/src/atoms/pagination/pagination.md +133 -0
- package/src/atoms/pagination/pagination.shared.tsx +289 -0
- package/src/atoms/pagination/pagination.styles.ts +245 -0
- package/src/atoms/pagination/pagination.tsx +6 -0
- package/src/atoms/popover/popover.android.tsx +8 -0
- package/src/atoms/popover/popover.ios.tsx +6 -0
- package/src/atoms/popover/popover.md +87 -0
- package/src/atoms/popover/popover.shared.tsx +124 -0
- package/src/atoms/popover/popover.styles.ts +144 -0
- package/src/atoms/popover/popover.tsx +6 -0
- package/src/atoms/radio/radio.android.tsx +6 -0
- package/src/atoms/radio/radio.ios.tsx +6 -0
- package/src/atoms/radio/radio.md +173 -0
- package/src/atoms/radio/radio.shared.tsx +98 -0
- package/src/atoms/radio/radio.styles.ts +109 -0
- package/src/atoms/radio/radio.tsx +6 -0
- package/src/atoms/select/select.android.tsx +6 -0
- package/src/atoms/select/select.ios.tsx +6 -0
- package/src/atoms/select/select.md +156 -0
- package/src/atoms/select/select.shared.tsx +143 -0
- package/src/atoms/select/select.styles.ts +310 -0
- package/src/atoms/select/select.tsx +6 -0
- package/src/atoms/skeleton/skeleton.md +135 -0
- package/src/atoms/skeleton/skeleton.styles.ts +117 -0
- package/src/atoms/skeleton/skeleton.tsx +145 -0
- package/src/atoms/spinner/spinner.android.tsx +7 -0
- package/src/atoms/spinner/spinner.ios.tsx +7 -0
- package/src/atoms/spinner/spinner.md +94 -0
- package/src/atoms/spinner/spinner.shared.tsx +92 -0
- package/src/atoms/spinner/spinner.styles.tsx +115 -0
- package/src/atoms/spinner/spinner.tsx +7 -0
- package/src/atoms/switch/switch.android.tsx +6 -0
- package/src/atoms/switch/switch.ios.tsx +6 -0
- package/src/atoms/switch/switch.md +91 -0
- package/src/atoms/switch/switch.shared.tsx +97 -0
- package/src/atoms/switch/switch.styles.ts +79 -0
- package/src/atoms/switch/switch.tsx +6 -0
- package/src/atoms/textarea/textarea.android.tsx +6 -0
- package/src/atoms/textarea/textarea.ios.tsx +6 -0
- package/src/atoms/textarea/textarea.md +140 -0
- package/src/atoms/textarea/textarea.shared.tsx +74 -0
- package/src/atoms/textarea/textarea.styles.ts +116 -0
- package/src/atoms/textarea/textarea.tsx +6 -0
- package/src/atoms/tooltip/tooltip.android.tsx +6 -0
- package/src/atoms/tooltip/tooltip.ios.tsx +7 -0
- package/src/atoms/tooltip/tooltip.md +122 -0
- package/src/atoms/tooltip/tooltip.shared.tsx +113 -0
- package/src/atoms/tooltip/tooltip.styles.ts +113 -0
- package/src/atoms/tooltip/tooltip.tsx +6 -0
- package/src/atoms/typography/typography.md +330 -0
- package/src/atoms/typography/typography.styles.ts +95 -0
- package/src/atoms/typography/typography.tsx +76 -0
- package/src/index.ts +12 -2
- package/src/molecules/action-panels/action-panels.md +133 -0
- package/src/molecules/action-panels/action-panels.styles.ts +39 -0
- package/src/molecules/action-panels/action-panels.tsx +113 -0
- package/src/molecules/alert/alert.md +119 -0
- package/src/molecules/alert/alert.styles.ts +88 -0
- package/src/molecules/alert/alert.tsx +74 -0
- package/src/molecules/alert-dialog/alert-dialog.android.tsx +6 -0
- package/src/molecules/alert-dialog/alert-dialog.ios.tsx +6 -0
- package/src/molecules/alert-dialog/alert-dialog.md +177 -0
- package/src/molecules/alert-dialog/alert-dialog.shared.tsx +187 -0
- package/src/molecules/alert-dialog/alert-dialog.styles.ts +248 -0
- package/src/molecules/alert-dialog/alert-dialog.tsx +6 -0
- package/src/molecules/card/card.md +190 -0
- package/src/molecules/card/card.styles.ts +67 -0
- package/src/molecules/card/card.tsx +176 -0
- package/src/molecules/code-block/code-block.md +159 -0
- package/src/molecules/code-block/code-block.styles.ts +167 -0
- package/src/molecules/code-block/code-block.tsx +176 -0
- package/src/molecules/description-lists/description-lists.md +129 -0
- package/src/molecules/description-lists/description-lists.styles.ts +102 -0
- package/src/molecules/description-lists/description-lists.tsx +133 -0
- package/src/molecules/empty-state/empty-state.md +218 -0
- package/src/molecules/empty-state/empty-state.styles.ts +63 -0
- package/src/molecules/empty-state/empty-state.tsx +77 -0
- package/src/molecules/feeds/feeds.md +102 -0
- package/src/molecules/feeds/feeds.styles.ts +120 -0
- package/src/molecules/feeds/feeds.tsx +167 -0
- package/src/molecules/field/field.md +117 -0
- package/src/molecules/field/field.styles.ts +85 -0
- package/src/molecules/field/field.tsx +175 -0
- package/src/molecules/fieldset/fieldset.md +141 -0
- package/src/molecules/fieldset/fieldset.styles.ts +79 -0
- package/src/molecules/fieldset/fieldset.tsx +182 -0
- package/src/molecules/form/form.md +137 -0
- package/src/molecules/form/form.styles.ts +39 -0
- package/src/molecules/form/form.tsx +246 -0
- package/src/molecules/grid-lists/grid-lists.md +114 -0
- package/src/molecules/grid-lists/grid-lists.styles.ts +79 -0
- package/src/molecules/grid-lists/grid-lists.tsx +157 -0
- package/src/molecules/index.ts +16 -0
- package/src/molecules/media-objects/media-objects.md +87 -0
- package/src/molecules/media-objects/media-objects.styles.ts +94 -0
- package/src/molecules/media-objects/media-objects.tsx +128 -0
- package/src/molecules/stacked-lists/stacked-lists.md +116 -0
- package/src/molecules/stacked-lists/stacked-lists.styles.ts +111 -0
- package/src/molecules/stacked-lists/stacked-lists.tsx +195 -0
- package/src/molecules/stats/stats.md +166 -0
- package/src/molecules/stats/stats.styles.ts +91 -0
- package/src/molecules/stats/stats.tsx +88 -0
- package/src/organisms/calendar/calendar.android.tsx +6 -0
- package/src/organisms/calendar/calendar.ios.tsx +6 -0
- package/src/organisms/calendar/calendar.md +114 -0
- package/src/organisms/calendar/calendar.shared.tsx +146 -0
- package/src/organisms/calendar/calendar.styles.ts +315 -0
- package/src/organisms/calendar/calendar.tsx +6 -0
- package/src/organisms/charts/charts.md +326 -0
- package/src/organisms/charts/charts.styles.ts +135 -0
- package/src/organisms/charts/charts.tsx +124 -0
- package/src/organisms/command/command.md +117 -0
- package/src/organisms/command/command.styles.ts +179 -0
- package/src/organisms/command/command.tsx +164 -0
- package/src/organisms/data-table/data-table.md +182 -0
- package/src/organisms/data-table/data-table.styles.ts +103 -0
- package/src/organisms/data-table/data-table.tsx +105 -0
- package/src/organisms/dialog/dialog.android.tsx +6 -0
- package/src/organisms/dialog/dialog.ios.tsx +6 -0
- package/src/organisms/dialog/dialog.md +271 -0
- package/src/organisms/dialog/dialog.shared.tsx +230 -0
- package/src/organisms/dialog/dialog.styles.ts +272 -0
- package/src/organisms/dialog/dialog.tsx +6 -0
- package/src/organisms/filter-panel/filter-panel.md +116 -0
- package/src/organisms/filter-panel/filter-panel.styles.ts +83 -0
- package/src/organisms/filter-panel/filter-panel.tsx +91 -0
- package/src/organisms/index.ts +13 -0
- package/src/organisms/navbars/navbars.android.tsx +6 -0
- package/src/organisms/navbars/navbars.ios.tsx +6 -0
- package/src/organisms/navbars/navbars.md +144 -0
- package/src/organisms/navbars/navbars.shared.tsx +137 -0
- package/src/organisms/navbars/navbars.styles.ts +251 -0
- package/src/organisms/navbars/navbars.tsx +6 -0
- package/src/organisms/overlays/overlays.android.tsx +6 -0
- package/src/organisms/overlays/overlays.ios.tsx +6 -0
- package/src/organisms/overlays/overlays.md +123 -0
- package/src/organisms/overlays/overlays.shared.tsx +175 -0
- package/src/organisms/overlays/overlays.styles.ts +309 -0
- package/src/organisms/overlays/overlays.tsx +6 -0
- package/src/organisms/row-menu/row-menu.android.tsx +6 -0
- package/src/organisms/row-menu/row-menu.ios.tsx +6 -0
- package/src/organisms/row-menu/row-menu.md +102 -0
- package/src/organisms/row-menu/row-menu.shared.tsx +105 -0
- package/src/organisms/row-menu/row-menu.styles.ts +262 -0
- package/src/organisms/row-menu/row-menu.tsx +6 -0
- package/src/organisms/sidebar/sidebar.android.tsx +6 -0
- package/src/organisms/sidebar/sidebar.ios.tsx +6 -0
- package/src/organisms/sidebar/sidebar.md +188 -0
- package/src/organisms/sidebar/sidebar.shared.tsx +167 -0
- package/src/organisms/sidebar/sidebar.styles.ts +262 -0
- package/src/organisms/sidebar/sidebar.tsx +6 -0
- package/src/organisms/stepper/stepper.android.tsx +6 -0
- package/src/organisms/stepper/stepper.ios.tsx +6 -0
- package/src/organisms/stepper/stepper.md +150 -0
- package/src/organisms/stepper/stepper.shared.tsx +158 -0
- package/src/organisms/stepper/stepper.styles.ts +280 -0
- package/src/organisms/stepper/stepper.tsx +6 -0
- package/src/organisms/tabs/tabs.android.tsx +6 -0
- package/src/organisms/tabs/tabs.ios.tsx +6 -0
- package/src/organisms/tabs/tabs.md +127 -0
- package/src/organisms/tabs/tabs.shared.tsx +281 -0
- package/src/organisms/tabs/tabs.styles.ts +398 -0
- package/src/organisms/tabs/tabs.tsx +6 -0
- package/src/style/color.ts +17 -0
- package/src/style/index.ts +14 -0
- package/src/style/primitives.ts +26 -0
- package/src/style/responsive.ts +45 -0
- package/src/style/shadow.ts +21 -0
- package/src/style/theme.tsx +56 -0
- package/src/style/tokens.ts +487 -0
- package/src/theme.ts +21 -0
- package/styles/canvas.css +128 -67
- package/tsconfig.json +4 -2
- package/src/cn.ts +0 -3
- package/styles/base.css +0 -17
- package/styles/components/alert.css +0 -66
- package/styles/components/app-shell.css +0 -46
- package/styles/components/avatar.css +0 -15
- package/styles/components/badge.css +0 -83
- package/styles/components/breadcrumb.css +0 -35
- package/styles/components/button-group.css +0 -23
- package/styles/components/button.css +0 -107
- package/styles/components/calendar.css +0 -73
- package/styles/components/card.css +0 -58
- package/styles/components/checkbox.css +0 -55
- package/styles/components/code-block.css +0 -18
- package/styles/components/combobox.css +0 -75
- package/styles/components/command.css +0 -94
- package/styles/components/data-table.css +0 -142
- package/styles/components/dialog.css +0 -72
- package/styles/components/dropdown.css +0 -54
- package/styles/components/empty-state.css +0 -17
- package/styles/components/field.css +0 -27
- package/styles/components/filter-panel.css +0 -58
- package/styles/components/form.css +0 -27
- package/styles/components/icon.css +0 -8
- package/styles/components/input-group.css +0 -45
- package/styles/components/input.css +0 -56
- package/styles/components/kbd.css +0 -15
- package/styles/components/page-header.css +0 -52
- package/styles/components/pagination.css +0 -48
- package/styles/components/popover.css +0 -14
- package/styles/components/radio.css +0 -28
- package/styles/components/row-menu.css +0 -69
- package/styles/components/section-card.css +0 -49
- package/styles/components/select.css +0 -57
- package/styles/components/separator.css +0 -32
- package/styles/components/sheet.css +0 -70
- package/styles/components/sidebar.css +0 -146
- package/styles/components/skeleton.css +0 -32
- package/styles/components/spinner.css +0 -26
- package/styles/components/stat-card.css +0 -71
- package/styles/components/stepper.css +0 -63
- package/styles/components/switch.css +0 -45
- package/styles/components/tabs.css +0 -40
- package/styles/components/textarea.css +0 -31
- package/styles/components/toast.css +0 -95
- package/styles/components/tooltip.css +0 -53
- package/styles/components/topbar.css +0 -24
- package/styles/components/typography.css +0 -105
- package/styles/patterns/backdrops.css +0 -35
- package/styles/patterns/density.css +0 -66
- package/styles/patterns/focus.css +0 -38
- package/styles/patterns/glass.css +0 -85
- package/styles/patterns/high-contrast.css +0 -70
- package/styles/patterns/reduced-motion.css +0 -12
- package/styles/patterns/scrollbar.css +0 -10
- package/styles/reset.css +0 -89
- package/styles/tokens/colors.css +0 -106
- package/styles/tokens/motion.css +0 -33
- package/styles/tokens/radius.css +0 -10
- package/styles/tokens/shadows.css +0 -35
- package/styles/tokens/spacing.css +0 -19
- package/styles/tokens/typography.css +0 -6
- package/styles/tokens/z-index.css +0 -12
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import { View, Pressable, Text, useTheme, type StyleProp, type ViewStyle } from "../../style/index.js";
|
|
3
|
+
import { Avatar } from "../../atoms/avatar/avatar.js";
|
|
4
|
+
import * as s from "./feeds.styles.js";
|
|
5
|
+
|
|
6
|
+
// An activity feed is a vertical timeline of events. Each item is a row with a
|
|
7
|
+
// leading mark (a small dot/initials node or a person's avatar), a content
|
|
8
|
+
// column carrying the actor + action + target line, and a muted timestamp.
|
|
9
|
+
//
|
|
10
|
+
// Two lead variants:
|
|
11
|
+
//
|
|
12
|
+
// 1. The connector feed (default): each row leads with a small bordered node
|
|
13
|
+
// and a vertical connector line links one event to the next. The connector
|
|
14
|
+
// is dropped on the final item so the line terminates cleanly at the last
|
|
15
|
+
// event rather than dangling past it.
|
|
16
|
+
// 2. The avatar feed (`avatar`): each row leads with the actor's avatar and the
|
|
17
|
+
// rows are separated by hairline rules instead of a connector line.
|
|
18
|
+
//
|
|
19
|
+
// Boolean-prop API: one boolean per option, grouped by axis, first-match
|
|
20
|
+
// precedence within an axis (mirrors Button's intentOf). The lead axis picks
|
|
21
|
+
// between the connector node and the avatar; `compact` is an orthogonal density
|
|
22
|
+
// modifier that tightens the vertical rhythm.
|
|
23
|
+
|
|
24
|
+
/** One event in the feed. */
|
|
25
|
+
export interface FeedItem {
|
|
26
|
+
/** Actor who performed the action, rendered bold (e.g. "Rachel Chen"). */
|
|
27
|
+
actor?: string;
|
|
28
|
+
/** The action text, muted (e.g. "approved the request"). */
|
|
29
|
+
action: string;
|
|
30
|
+
/** Optional target of the action, muted and trailing the action. */
|
|
31
|
+
target?: string;
|
|
32
|
+
/** Relative timestamp, muted and small (e.g. "2 hours ago"). */
|
|
33
|
+
time: string;
|
|
34
|
+
/** Photo URL for the avatar lead; falls back to initials from the actor. */
|
|
35
|
+
avatar?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface FeedProps {
|
|
39
|
+
/** Events to render, top to bottom. */
|
|
40
|
+
items?: FeedItem[];
|
|
41
|
+
// Lead axis (pick one; default is the connector node + vertical line).
|
|
42
|
+
connector?: boolean;
|
|
43
|
+
avatar?: boolean;
|
|
44
|
+
// Density modifier: tightens the row padding and connector spacing.
|
|
45
|
+
compact?: boolean;
|
|
46
|
+
/** When set, each event row is pressable, reporting the row index. */
|
|
47
|
+
onItemPress?: (index: number) => void;
|
|
48
|
+
/** Escape hatch for layout/positioning composition (mainly width). */
|
|
49
|
+
style?: StyleProp<ViewStyle>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
type Lead = "connector" | "avatar";
|
|
53
|
+
|
|
54
|
+
// Lead precedence when more than one is passed: first match wins.
|
|
55
|
+
function leadOf(p: FeedProps): Lead {
|
|
56
|
+
if (p.connector) return "connector";
|
|
57
|
+
if (p.avatar) return "avatar";
|
|
58
|
+
return "connector";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Two initials from an actor name, used for the avatar/node fallback when no
|
|
62
|
+
// photo is supplied (e.g. "Rachel Chen" -> "RC").
|
|
63
|
+
function initialsFrom(name: string): string {
|
|
64
|
+
const parts = name.trim().split(/\s+/).filter(Boolean);
|
|
65
|
+
if (parts.length === 0) return "";
|
|
66
|
+
if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase();
|
|
67
|
+
return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function Feed(props: FeedProps) {
|
|
71
|
+
const { items = [], onItemPress, style } = props;
|
|
72
|
+
const { tokens } = useTheme();
|
|
73
|
+
const lead = leadOf(props);
|
|
74
|
+
const compact = !!props.compact;
|
|
75
|
+
const lastIndex = items.length - 1;
|
|
76
|
+
|
|
77
|
+
const renderContent = (item: FeedItem) => (
|
|
78
|
+
<View style={s.contentColumn}>
|
|
79
|
+
<Text style={s.lineText}>
|
|
80
|
+
{item.actor ? <Text style={s.actorLabel(tokens)}>{item.actor} </Text> : null}
|
|
81
|
+
<Text style={s.actionLabel(tokens)}>{item.action}</Text>
|
|
82
|
+
{item.target ? <Text style={s.actionLabel(tokens)}> {item.target}</Text> : null}
|
|
83
|
+
</Text>
|
|
84
|
+
<Text style={s.timeLabel(tokens)}>{item.time}</Text>
|
|
85
|
+
</View>
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
if (lead === "avatar") {
|
|
89
|
+
// Avatar lead: each row leads with the actor's avatar; rows are ruled by a
|
|
90
|
+
// hairline between items (the last row keeps no rule).
|
|
91
|
+
const rows = items.map((item, index) => {
|
|
92
|
+
const divider = index < lastIndex ? s.avatarDivider(tokens) : null;
|
|
93
|
+
const rowStyle: StyleProp<ViewStyle> = [s.avatarRow, s.avatarRowPad(compact), divider];
|
|
94
|
+
const inner: ReactNode = (
|
|
95
|
+
<>
|
|
96
|
+
<Avatar src={item.avatar} name={item.actor}>
|
|
97
|
+
{item.actor ? initialsFrom(item.actor) : ""}
|
|
98
|
+
</Avatar>
|
|
99
|
+
{renderContent(item)}
|
|
100
|
+
</>
|
|
101
|
+
);
|
|
102
|
+
if (onItemPress) {
|
|
103
|
+
return (
|
|
104
|
+
<Pressable
|
|
105
|
+
key={index}
|
|
106
|
+
accessibilityRole="button"
|
|
107
|
+
onPress={() => onItemPress(index)}
|
|
108
|
+
style={({ pressed }) => [rowStyle, pressed ? { backgroundColor: tokens.accent } : null]}
|
|
109
|
+
>
|
|
110
|
+
{inner}
|
|
111
|
+
</Pressable>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
return (
|
|
115
|
+
<View key={index} style={rowStyle}>
|
|
116
|
+
{inner}
|
|
117
|
+
</View>
|
|
118
|
+
);
|
|
119
|
+
});
|
|
120
|
+
return <View style={[s.cardSurface(tokens), style]}>{rows}</View>;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Connector lead: a bordered node per row with a vertical line linking each
|
|
124
|
+
// event to the next. The line is dropped on the final item.
|
|
125
|
+
const rows = items.map((item, index) => {
|
|
126
|
+
const isLast = index === lastIndex;
|
|
127
|
+
const rowStyle: StyleProp<ViewStyle> = [s.connectorRow, isLast ? null : s.connectorRowGap(compact)];
|
|
128
|
+
const inner: ReactNode = (
|
|
129
|
+
<>
|
|
130
|
+
{!isLast ? (
|
|
131
|
+
// Vertical connector: a 1px border-colored line running from just below
|
|
132
|
+
// the node down to the next row. Absolutely placed under the node's
|
|
133
|
+
// horizontal center (node is 28px wide -> center at 14px, minus the
|
|
134
|
+
// 0.5px line half-width lands at 13px).
|
|
135
|
+
<View style={s.connectorLine(tokens)} />
|
|
136
|
+
) : null}
|
|
137
|
+
<View style={s.node(tokens)}>
|
|
138
|
+
{item.actor ? (
|
|
139
|
+
<Text style={s.nodeInitials(tokens)}>{initialsFrom(item.actor)}</Text>
|
|
140
|
+
) : (
|
|
141
|
+
<View style={s.nodeDot(tokens)} />
|
|
142
|
+
)}
|
|
143
|
+
</View>
|
|
144
|
+
<View style={s.connectorContentColumn}>{renderContent(item)}</View>
|
|
145
|
+
</>
|
|
146
|
+
);
|
|
147
|
+
if (onItemPress) {
|
|
148
|
+
return (
|
|
149
|
+
<Pressable
|
|
150
|
+
key={index}
|
|
151
|
+
accessibilityRole="button"
|
|
152
|
+
onPress={() => onItemPress(index)}
|
|
153
|
+
style={({ pressed }) => [rowStyle, pressed ? { backgroundColor: tokens.accent } : null]}
|
|
154
|
+
>
|
|
155
|
+
{inner}
|
|
156
|
+
</Pressable>
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
return (
|
|
160
|
+
<View key={index} style={rowStyle}>
|
|
161
|
+
{inner}
|
|
162
|
+
</View>
|
|
163
|
+
);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
return <View style={[s.cardSurface(tokens), s.connectorPad(compact), style]}>{rows}</View>;
|
|
167
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# Field Display
|
|
2
|
+
|
|
3
|
+
Read-only key/value pairs. Used in detail views, modal previews, and audit screens. Optional mono mode for IDs, tokens, dates.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
<Field
|
|
9
|
+
rows={[
|
|
10
|
+
{ label: "User ID", value: "usr_abc123", mono: true },
|
|
11
|
+
{ label: "Name", value: "Rachel Chen" },
|
|
12
|
+
{ label: "Role", value: "Admin" },
|
|
13
|
+
{ label: "Status", status: "Active" }
|
|
14
|
+
]}
|
|
15
|
+
style={{ maxWidth: 400 }}
|
|
16
|
+
/>
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Variants
|
|
20
|
+
|
|
21
|
+
### Value mode - mono
|
|
22
|
+
|
|
23
|
+
```tsx
|
|
24
|
+
<Field
|
|
25
|
+
rows={[
|
|
26
|
+
{ label: "Client ID", value: "clt_8f2a9b4c7e1d", mono: true },
|
|
27
|
+
{ label: "Created", value: "2026-05-24T14:32:00Z", mono: true },
|
|
28
|
+
{ label: "Fingerprint", value: "sha256:xK9v...", mono: true }
|
|
29
|
+
]}
|
|
30
|
+
style={{ maxWidth: 400 }}
|
|
31
|
+
/>
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Value mode - composed
|
|
35
|
+
|
|
36
|
+
```tsx
|
|
37
|
+
<Field
|
|
38
|
+
rows={[
|
|
39
|
+
{ label: "Status", status: "Active" },
|
|
40
|
+
{ label: "Plan", badge: "Pro" },
|
|
41
|
+
{ label: "Token", value: "sk_live_a8f2...c9e1", mono: true, copyValue: "sk_live_a8f2c9e1" },
|
|
42
|
+
{ label: "Members", avatars: [
|
|
43
|
+
{ src: "/rachel-chen.jpg", name: "RC" },
|
|
44
|
+
{ name: "AJ" }
|
|
45
|
+
], overflow: 3 }
|
|
46
|
+
]}
|
|
47
|
+
style={{ maxWidth: 400 }}
|
|
48
|
+
/>
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Do & Don't
|
|
52
|
+
|
|
53
|
+
### Basic
|
|
54
|
+
|
|
55
|
+
**Do** — Use the fixed 180px label column so every value aligns to one baseline.
|
|
56
|
+
|
|
57
|
+
```tsx
|
|
58
|
+
<Field style={{ maxWidth: 400 }} rows={[
|
|
59
|
+
{ label: "Name", value: "Rachel Chen" },
|
|
60
|
+
{ label: "Role", value: "Admin" }
|
|
61
|
+
]} />
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
**Don't** — Inline label-colon-value with no shared column makes values ragged and impossible to scan down a list.
|
|
65
|
+
|
|
66
|
+
```tsx
|
|
67
|
+
<View style={{ maxWidth: 400, flexDirection: "column", gap: 4 }}>
|
|
68
|
+
<Text style={{ fontSize: 14, lineHeight: 20 }}>
|
|
69
|
+
<Text style={{ fontWeight: "600" }}>Name:</Text>
|
|
70
|
+
Rachel Chen
|
|
71
|
+
</Text>
|
|
72
|
+
<Text style={{ fontSize: 14, lineHeight: 20 }}>
|
|
73
|
+
<Text style={{ fontWeight: "600" }}>Role:</Text>
|
|
74
|
+
Admin
|
|
75
|
+
</Text>
|
|
76
|
+
</View>
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Mono
|
|
80
|
+
|
|
81
|
+
**Do** — Wrap IDs, hashes, and timestamps in font-mono so every glyph is fixed-width and copy-able.
|
|
82
|
+
|
|
83
|
+
```tsx
|
|
84
|
+
<Field style={{ maxWidth: 400 }} rows={[
|
|
85
|
+
{ label: "Client ID", value: "clt_8f2a9b4c7e1d", mono: true },
|
|
86
|
+
{ label: "Fingerprint", value: "sha256:xK9v...", mono: true }
|
|
87
|
+
]} />
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
**Don't** — Rendering IDs and hashes in proportional type makes look-alike characters (l/1, O/0) hard to compare.
|
|
91
|
+
|
|
92
|
+
```tsx
|
|
93
|
+
<Field style={{ maxWidth: 400 }} rows={[
|
|
94
|
+
{ label: "Client ID", value: "clt_8f2a9b4c7e1d" },
|
|
95
|
+
{ label: "Fingerprint", value: "sha256:xK9v..." }
|
|
96
|
+
]} />
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Composed
|
|
100
|
+
|
|
101
|
+
**Do** — Compose real nodes into the value slot: a status badge for state, a badge for the plan tier.
|
|
102
|
+
|
|
103
|
+
```tsx
|
|
104
|
+
<Field style={{ maxWidth: 400 }} rows={[
|
|
105
|
+
{ label: "Status", status: "Active" },
|
|
106
|
+
{ label: "Plan", badge: "Pro" }
|
|
107
|
+
]} />
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
**Don't** — Flattening a live status or plan tier to plain text drops the color and shape that signal state at a glance.
|
|
111
|
+
|
|
112
|
+
```tsx
|
|
113
|
+
<Field style={{ maxWidth: 400 }} rows={[
|
|
114
|
+
{ label: "Status", value: "Active" },
|
|
115
|
+
{ label: "Plan", value: "Pro" }
|
|
116
|
+
]} />
|
|
117
|
+
```
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { type ViewStyle, type TextStyle } from "react-native";
|
|
2
|
+
import { type ColorTokens } from "../../style/index.js";
|
|
3
|
+
|
|
4
|
+
// Co-located Field styles. Layout-only fragments are static objects; anything
|
|
5
|
+
// that reads a color is a function of the active tokens, so the label/value text
|
|
6
|
+
// and the avatar-overflow chip follow light/dark (and the glass surface).
|
|
7
|
+
|
|
8
|
+
// --- shared text ------------------------------------------------------------
|
|
9
|
+
|
|
10
|
+
// The engine has no font-family utility, so request RN's cross-platform
|
|
11
|
+
// monospace alias via inline style (the same technique Badge uses for `mono`).
|
|
12
|
+
export const monoStyle: TextStyle = { fontFamily: "monospace" };
|
|
13
|
+
|
|
14
|
+
// Left-column label of a display row: a fixed 180px muted term.
|
|
15
|
+
export function fieldLabel(tokens: ColorTokens): TextStyle {
|
|
16
|
+
return { width: 180, flexShrink: 0, fontSize: 14, lineHeight: 20, color: tokens["muted-foreground"] };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// The default value shape: medium-weight foreground text.
|
|
20
|
+
export function fieldValue(tokens: ColorTokens): TextStyle {
|
|
21
|
+
return { fontSize: 14, lineHeight: 20, fontWeight: "500", color: tokens.foreground };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// --- display mode (rows) ----------------------------------------------------
|
|
25
|
+
|
|
26
|
+
// Outer stack of label/value rows.
|
|
27
|
+
export const displayStack: ViewStyle = { flexDirection: "column", gap: 12 };
|
|
28
|
+
|
|
29
|
+
// One row: label column + value, baseline-aligned.
|
|
30
|
+
export const displayRow: ViewStyle = { flexDirection: "row", alignItems: "center", gap: 16 };
|
|
31
|
+
|
|
32
|
+
// The value column grows to fill the row beside the fixed label column.
|
|
33
|
+
export const valueFill: ViewStyle = { flexGrow: 1, flexShrink: 1, flexBasis: "0%" };
|
|
34
|
+
|
|
35
|
+
// An overlapping avatar stack row.
|
|
36
|
+
export const avatarRow: ViewStyle = { flexDirection: "row", alignItems: "center" };
|
|
37
|
+
|
|
38
|
+
// Each avatar after the first overlaps the previous one.
|
|
39
|
+
export const avatarOverlap: ViewStyle = { marginLeft: -8 };
|
|
40
|
+
|
|
41
|
+
// The trailing "+N" overflow chip after an avatar stack.
|
|
42
|
+
export function overflowChip(tokens: ColorTokens): ViewStyle {
|
|
43
|
+
return {
|
|
44
|
+
marginLeft: -8,
|
|
45
|
+
height: 28,
|
|
46
|
+
width: 28,
|
|
47
|
+
alignItems: "center",
|
|
48
|
+
justifyContent: "center",
|
|
49
|
+
borderRadius: 9999,
|
|
50
|
+
borderWidth: 2,
|
|
51
|
+
borderColor: tokens.background,
|
|
52
|
+
backgroundColor: tokens.muted,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function overflowText(tokens: ColorTokens): TextStyle {
|
|
57
|
+
return { fontSize: 12, lineHeight: 16, fontWeight: "500", color: tokens["muted-foreground"] };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// A copyable value: the value text followed by a ghost Copy button.
|
|
61
|
+
export const copyRow: ViewStyle = { flexDirection: "row", alignItems: "center", gap: 8 };
|
|
62
|
+
|
|
63
|
+
// --- control mode -----------------------------------------------------------
|
|
64
|
+
|
|
65
|
+
// Outer stack of label / control / message.
|
|
66
|
+
export const controlStack: ViewStyle = { flexDirection: "column", gap: 6 };
|
|
67
|
+
|
|
68
|
+
// The dimmed look applied to the whole field when disabled.
|
|
69
|
+
export const dimmed: ViewStyle = { opacity: 0.5 };
|
|
70
|
+
|
|
71
|
+
// The control's label above the Input.
|
|
72
|
+
export function label(tokens: ColorTokens): TextStyle {
|
|
73
|
+
return { fontSize: 14, lineHeight: 20, fontWeight: "500", color: tokens.foreground };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// The destructive "*" appended to a required field's label.
|
|
77
|
+
export function requiredMark(tokens: ColorTokens): TextStyle {
|
|
78
|
+
return { color: tokens.destructive };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// The helper / error line below the control. Error text is destructive, the
|
|
82
|
+
// resting helper is muted.
|
|
83
|
+
export function message(tokens: ColorTokens, error: boolean): TextStyle {
|
|
84
|
+
return { fontSize: 12, lineHeight: 16, color: error ? tokens.destructive : tokens["muted-foreground"] };
|
|
85
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { View, Text, useTheme, type StyleProp, type ViewStyle } from "../../style/index.js";
|
|
2
|
+
import { Avatar } from "../../atoms/avatar/avatar.js";
|
|
3
|
+
import { Badge } from "../../atoms/badge/badge.js";
|
|
4
|
+
import { Button } from "../../atoms/button/button.js";
|
|
5
|
+
import { Input } from "../../atoms/input/input.js";
|
|
6
|
+
import * as s from "./field.styles.js";
|
|
7
|
+
|
|
8
|
+
/** One avatar in a Members stack: a photo (`src`) or initials (`name`). */
|
|
9
|
+
export interface FieldAvatar {
|
|
10
|
+
src?: string;
|
|
11
|
+
name?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* One row of the read-only Field display: a label paired with a value. The
|
|
16
|
+
* value slot is data-driven and composes real atoms; pass at most one value
|
|
17
|
+
* shape per row, resolved in this precedence: avatars > copyValue > status >
|
|
18
|
+
* badge > plain value.
|
|
19
|
+
*/
|
|
20
|
+
export interface FieldRow {
|
|
21
|
+
/** Left-column label (the muted term). */
|
|
22
|
+
label: string;
|
|
23
|
+
/** Plain text value (the default value shape). */
|
|
24
|
+
value?: string;
|
|
25
|
+
/** Render `value`/`copyValue` in a fixed-width monospace face (IDs, tokens, hashes). */
|
|
26
|
+
mono?: boolean;
|
|
27
|
+
/** Render the value as a metadata Badge (secondary tone) carrying this text, e.g. a plan tier. */
|
|
28
|
+
badge?: string;
|
|
29
|
+
/** Render the value as a success status Badge carrying this text, e.g. "Active". */
|
|
30
|
+
status?: string;
|
|
31
|
+
/** Append a ghost "Copy" button after the value that copies this string. */
|
|
32
|
+
copyValue?: string;
|
|
33
|
+
/** Render the value as an overlapping avatar stack. */
|
|
34
|
+
avatars?: FieldAvatar[];
|
|
35
|
+
/** Trailing "+N" overflow chip after an avatar stack. */
|
|
36
|
+
overflow?: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface FieldProps {
|
|
40
|
+
/**
|
|
41
|
+
* Read-only key/value rows. When set, Field renders the field-display
|
|
42
|
+
* (label column + composed value) instead of the editable input control.
|
|
43
|
+
*/
|
|
44
|
+
rows?: FieldRow[];
|
|
45
|
+
/** Label shown above the control. */
|
|
46
|
+
label?: string;
|
|
47
|
+
/** Helper text shown below the control in the resting state. */
|
|
48
|
+
helper?: string;
|
|
49
|
+
/** Error message shown below the control when `error` is set; replaces the helper. */
|
|
50
|
+
error?: string;
|
|
51
|
+
/** Placeholder forwarded to the wrapped Input. */
|
|
52
|
+
placeholder?: string;
|
|
53
|
+
/** Current text value (controlled), forwarded to the Input. */
|
|
54
|
+
value?: string;
|
|
55
|
+
/** Called with the new text on each keystroke, forwarded to the Input. */
|
|
56
|
+
onChangeText?: (text: string) => void;
|
|
57
|
+
// Boolean axes (orthogonal, stack freely).
|
|
58
|
+
/** Marks the field as required: appends a destructive "*" to the label. */
|
|
59
|
+
required?: boolean;
|
|
60
|
+
/** Disables the control and dims the whole field. */
|
|
61
|
+
disabled?: boolean;
|
|
62
|
+
/** Invalid state: shows the error message (red) and flags the Input. */
|
|
63
|
+
invalid?: boolean;
|
|
64
|
+
/** Escape hatch for layout/positioning composition (mainly width). */
|
|
65
|
+
style?: StyleProp<ViewStyle>;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Render a row's value slot from its data descriptor. Precedence: an avatar
|
|
69
|
+
// stack, then a copyable value, then a status badge, then a metadata badge,
|
|
70
|
+
// then plain (optionally monospace) text.
|
|
71
|
+
function FieldValue(row: FieldRow) {
|
|
72
|
+
const { tokens } = useTheme();
|
|
73
|
+
|
|
74
|
+
if (row.avatars && row.avatars.length > 0) {
|
|
75
|
+
return (
|
|
76
|
+
<View style={s.avatarRow}>
|
|
77
|
+
{row.avatars.map((a, i) => (
|
|
78
|
+
<View key={i} style={i > 0 ? s.avatarOverlap : undefined}>
|
|
79
|
+
<Avatar small ring src={a.src} name={a.name}>
|
|
80
|
+
{a.name}
|
|
81
|
+
</Avatar>
|
|
82
|
+
</View>
|
|
83
|
+
))}
|
|
84
|
+
{typeof row.overflow === "number" && row.overflow > 0 ? (
|
|
85
|
+
<View style={s.overflowChip(tokens)}>
|
|
86
|
+
<Text style={s.overflowText(tokens)}>{`+${row.overflow}`}</Text>
|
|
87
|
+
</View>
|
|
88
|
+
) : null}
|
|
89
|
+
</View>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
if (row.copyValue != null) {
|
|
93
|
+
return (
|
|
94
|
+
<View style={s.copyRow}>
|
|
95
|
+
<Text style={[s.fieldValue(tokens), row.mono ? s.monoStyle : null]}>
|
|
96
|
+
{row.value ?? row.copyValue}
|
|
97
|
+
</Text>
|
|
98
|
+
<Button ghost small>
|
|
99
|
+
Copy
|
|
100
|
+
</Button>
|
|
101
|
+
</View>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
if (row.status != null) {
|
|
105
|
+
return (
|
|
106
|
+
<Badge status success>
|
|
107
|
+
{row.status}
|
|
108
|
+
</Badge>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
if (row.badge != null) {
|
|
112
|
+
return <Badge secondary>{row.badge}</Badge>;
|
|
113
|
+
}
|
|
114
|
+
return (
|
|
115
|
+
<Text style={[s.fieldValue(tokens), row.mono ? s.monoStyle : null]}>
|
|
116
|
+
{row.value}
|
|
117
|
+
</Text>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function Field(props: FieldProps) {
|
|
122
|
+
const {
|
|
123
|
+
rows,
|
|
124
|
+
label,
|
|
125
|
+
helper,
|
|
126
|
+
error,
|
|
127
|
+
placeholder,
|
|
128
|
+
value,
|
|
129
|
+
onChangeText,
|
|
130
|
+
required,
|
|
131
|
+
disabled,
|
|
132
|
+
invalid,
|
|
133
|
+
style,
|
|
134
|
+
} = props;
|
|
135
|
+
const { tokens } = useTheme();
|
|
136
|
+
|
|
137
|
+
// Display mode: a read-only stack of label/value rows. Each row aligns its
|
|
138
|
+
// label to a fixed 180px column (flex, not grid) so every value lines up to
|
|
139
|
+
// one baseline.
|
|
140
|
+
if (rows) {
|
|
141
|
+
return (
|
|
142
|
+
<View style={[s.displayStack, disabled ? s.dimmed : null, style]}>
|
|
143
|
+
{rows.map((row, index) => (
|
|
144
|
+
<View key={`${row.label}-${index}`} style={s.displayRow}>
|
|
145
|
+
<Text style={s.fieldLabel(tokens)}>{row.label}</Text>
|
|
146
|
+
<View style={s.valueFill}>{FieldValue(row)}</View>
|
|
147
|
+
</View>
|
|
148
|
+
))}
|
|
149
|
+
</View>
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Error takes precedence over the resting helper below the control.
|
|
154
|
+
const showError = !!invalid && !!error;
|
|
155
|
+
const messageText = showError ? error : helper;
|
|
156
|
+
|
|
157
|
+
return (
|
|
158
|
+
<View style={[s.controlStack, disabled ? s.dimmed : null, style]}>
|
|
159
|
+
{label != null ? (
|
|
160
|
+
<Text style={s.label(tokens)}>
|
|
161
|
+
{label}
|
|
162
|
+
{required ? <Text style={s.requiredMark(tokens)}> *</Text> : null}
|
|
163
|
+
</Text>
|
|
164
|
+
) : null}
|
|
165
|
+
<Input
|
|
166
|
+
value={value}
|
|
167
|
+
onChangeText={onChangeText}
|
|
168
|
+
placeholder={placeholder}
|
|
169
|
+
disabled={disabled}
|
|
170
|
+
error={invalid}
|
|
171
|
+
/>
|
|
172
|
+
{messageText != null ? <Text style={s.message(tokens, showError)}>{messageText}</Text> : null}
|
|
173
|
+
</View>
|
|
174
|
+
);
|
|
175
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# Fieldsets
|
|
2
|
+
|
|
3
|
+
Group related form controls under a legend. Each field pairs a label, control, optional help text, and an inline error, so a set of inputs reads as one labeled unit.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
<Fieldset
|
|
9
|
+
legend="Shipping details"
|
|
10
|
+
description="Where should we send your order?"
|
|
11
|
+
items={[
|
|
12
|
+
{ label: "Full name", placeholder: "Ada Lovelace" },
|
|
13
|
+
{ label: "Email", placeholder: "ada@example.com", value: "ada@", help: "We'll only use this for order updates." },
|
|
14
|
+
{ label: "Country", placeholder: "United States" }
|
|
15
|
+
]}
|
|
16
|
+
/>
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Variants
|
|
20
|
+
|
|
21
|
+
### Content - checkboxes
|
|
22
|
+
|
|
23
|
+
```tsx
|
|
24
|
+
<Fieldset
|
|
25
|
+
legend="Email notifications"
|
|
26
|
+
description="Choose what we email you about."
|
|
27
|
+
checkboxes={[
|
|
28
|
+
{ label: "Product updates", checked: true },
|
|
29
|
+
{ label: "Security alerts", checked: true },
|
|
30
|
+
{ label: "Weekly digest", checked: false }
|
|
31
|
+
]}
|
|
32
|
+
/>
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Validation error
|
|
36
|
+
|
|
37
|
+
```tsx
|
|
38
|
+
<Fieldset
|
|
39
|
+
legend="Shipping details"
|
|
40
|
+
description="Where should we send your order?"
|
|
41
|
+
items={[
|
|
42
|
+
{ label: "Full name", placeholder: "Ada Lovelace" },
|
|
43
|
+
{ label: "Email", placeholder: "ada@example.com", value: "ada@", help: "We'll only use this for order updates.", error: "Enter a valid email address" },
|
|
44
|
+
{ label: "Country", placeholder: "United States" }
|
|
45
|
+
]}
|
|
46
|
+
/>
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Disabled
|
|
50
|
+
|
|
51
|
+
```tsx
|
|
52
|
+
<Fieldset
|
|
53
|
+
legend="Shipping details"
|
|
54
|
+
description="Where should we send your order?"
|
|
55
|
+
disabled
|
|
56
|
+
items={[
|
|
57
|
+
{ label: "Full name", placeholder: "Ada Lovelace" },
|
|
58
|
+
{ label: "Email", placeholder: "ada@example.com", value: "ada@", help: "We'll only use this for order updates." },
|
|
59
|
+
{ label: "Country", placeholder: "United States" }
|
|
60
|
+
]}
|
|
61
|
+
/>
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Columns - 2
|
|
65
|
+
|
|
66
|
+
```tsx
|
|
67
|
+
<Fieldset
|
|
68
|
+
legend="Shipping details"
|
|
69
|
+
description="Where should we send your order?"
|
|
70
|
+
twoColumn
|
|
71
|
+
items={[
|
|
72
|
+
{ label: "Full name", placeholder: "Ada Lovelace" },
|
|
73
|
+
{ label: "Email", placeholder: "ada@example.com", value: "ada@", help: "We'll only use this for order updates." },
|
|
74
|
+
{ label: "Country", placeholder: "United States" }
|
|
75
|
+
]}
|
|
76
|
+
/>
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Do & Don't
|
|
80
|
+
|
|
81
|
+
### Text fields
|
|
82
|
+
|
|
83
|
+
**Do** — Give every field a persistent label; use the placeholder only for an example value or format hint.
|
|
84
|
+
|
|
85
|
+
```tsx
|
|
86
|
+
<Fieldset legend="Shipping details" items={[
|
|
87
|
+
{ label: "Full name", placeholder: "Ada Lovelace" },
|
|
88
|
+
{ label: "Email", placeholder: "ada@example.com", help: "We'll only use this for order updates." }
|
|
89
|
+
]} />
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
**Don't** — A placeholder is not a label: it vanishes on focus and is skipped by many screen readers, leaving the field unnamed.
|
|
93
|
+
|
|
94
|
+
```tsx
|
|
95
|
+
<Fieldset legend="Shipping details" items={[
|
|
96
|
+
{ label: "", placeholder: "Full name" },
|
|
97
|
+
{ label: "", placeholder: "Email" }
|
|
98
|
+
]} />
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Checkbox group
|
|
102
|
+
|
|
103
|
+
**Do** — A legend names the group so its checkboxes read as one labeled unit.
|
|
104
|
+
|
|
105
|
+
```tsx
|
|
106
|
+
<Fieldset legend="Notify me by" checkboxes={[
|
|
107
|
+
{ label: "Email" },
|
|
108
|
+
{ label: "SMS" },
|
|
109
|
+
{ label: "Push" }
|
|
110
|
+
]} />
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
**Don't** — Without a legend the relationship between the controls is implicit; screen readers announce them as unrelated.
|
|
114
|
+
|
|
115
|
+
```tsx
|
|
116
|
+
<Fieldset checkboxes={[
|
|
117
|
+
{ label: "Email" },
|
|
118
|
+
{ label: "SMS" },
|
|
119
|
+
{ label: "Push" }
|
|
120
|
+
]} />
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Two-column
|
|
124
|
+
|
|
125
|
+
**Do** — Pair only naturally adjacent, short fields side by side; the grid stacks to one column on small screens.
|
|
126
|
+
|
|
127
|
+
```tsx
|
|
128
|
+
<Fieldset legend="Card details" twoColumn items={[
|
|
129
|
+
{ label: "Expiry", placeholder: "MM / YY" },
|
|
130
|
+
{ label: "CVC", placeholder: "123" }
|
|
131
|
+
]} />
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
**Don't** — Splitting unrelated or full-width fields across two columns crams the form and breaks the reading order.
|
|
135
|
+
|
|
136
|
+
```tsx
|
|
137
|
+
<Fieldset legend="Account" twoColumn items={[
|
|
138
|
+
{ label: "Bio", value: "Engineering lead." },
|
|
139
|
+
{ label: "Country", value: "United States" }
|
|
140
|
+
]} />
|
|
141
|
+
```
|