astryx-dracula 0.2.1 → 0.3.1
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/BRAND.md +30 -18
- package/README.md +17 -12
- package/USAGE.md +30 -10
- package/astryx-dracula.js +19 -13
- package/astryx-dracula.variants.d.ts +15 -0
- package/package.json +59 -17
- package/shared/auth-chrome-config.ts +18 -0
- package/shared/auth-chrome.tsx +25 -0
- package/shared/auth-copy.ts +22 -0
- package/shared/chaptered-doc-config.ts +92 -0
- package/shared/chaptered-doc.tsx +332 -0
- package/shared/chart-hues.ts +27 -0
- package/shared/chart-labels.tsx +44 -0
- package/shared/chart-legend.tsx +100 -0
- package/shared/chart-panel-style.ts +13 -0
- package/shared/data-bar.tsx +160 -0
- package/shared/gallery-image.ts +20 -0
- package/shared/login-demo.ts +7 -0
- package/shared/metric-delta.tsx +36 -0
- package/shared/order-desk.tsx +273 -0
- package/shared/revenue-chart.tsx +181 -0
- package/shared/scene-castle.tsx +322 -0
- package/shared/scene-frame.tsx +88 -0
- package/shared/scene-hues.ts +49 -0
- package/shared/scene-tile.tsx +128 -0
- package/shared/settings-data.ts +125 -0
- package/shared/settings-rows.tsx +179 -0
- package/shared/sparkline.tsx +102 -0
- package/shared/sso-icons.tsx +30 -0
- package/templates/ai-chat-landing.tsx +66 -33
- package/templates/ai-chat.tsx +36 -19
- package/templates/blank.tsx +4 -3
- package/templates/centered-hero.tsx +14 -125
- package/templates/classic-gallery.tsx +20 -66
- package/templates/contact-form.tsx +59 -39
- package/templates/dashboard-portfolio.tsx +143 -226
- package/templates/dashboard.tsx +122 -122
- package/templates/detail-page.tsx +95 -91
- package/templates/documentation-design.template.mjs +1 -1
- package/templates/documentation-design.tsx +83 -96
- package/templates/documentation-technical.template.mjs +1 -1
- package/templates/documentation-technical.tsx +29 -19
- package/templates/documentation.template.mjs +1 -1
- package/templates/documentation.tsx +52 -74
- package/templates/editor.tsx +112 -84
- package/templates/file-explorer.tsx +131 -84
- package/templates/form-two-column.tsx +61 -125
- package/templates/gallery-hero.tsx +15 -171
- package/templates/ide.tsx +22 -20
- package/templates/incident-console.tsx +104 -70
- package/templates/kanban-board.tsx +99 -42
- package/templates/library.template.mjs +2 -2
- package/templates/library.tsx +37 -42
- package/templates/login-card.tsx +58 -87
- package/templates/login-split.tsx +88 -126
- package/templates/login-sso.tsx +105 -64
- package/templates/login.tsx +107 -53
- package/templates/messaging-shell.tsx +171 -144
- package/templates/mixed-gallery.tsx +29 -130
- package/templates/payment-form.tsx +368 -290
- package/templates/product-detail.tsx +39 -58
- package/templates/product-gallery.tsx +30 -41
- package/templates/product-tour.template.mjs +6 -0
- package/templates/product-tour.tsx +916 -0
- package/templates/settings-dialog.tsx +135 -282
- package/templates/settings-sidebar.tsx +178 -268
- package/templates/settings.tsx +41 -14
- package/templates/shell-nav.tsx +31 -24
- package/templates/shell-side-nav.tsx +29 -20
- package/templates/shell-top-nav.tsx +80 -44
- package/templates/side-gallery.tsx +38 -94
- package/templates/table-grouped.tsx +62 -34
- package/templates/table-page-chart.tsx +23 -302
- package/templates/table-page-heatmap-status.tsx +156 -92
- package/templates/table-page-shoe-store-heatmap.tsx +18 -293
- package/templates/table-page.tsx +23 -7
- package/templates/table.tsx +23 -8
- package/templates/tech-report.template.mjs +6 -0
- package/templates/tech-report.tsx +1486 -0
- package/templates/theme-showcase.template.mjs +1 -1
- package/templates/theme-showcase.tsx +299 -237
- package/theme.css +76 -14
- package/tokens.css +433 -30
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// Settings row DATA, split from the row renderers in settings-rows.tsx.
|
|
2
|
+
//
|
|
3
|
+
// WHY: settings-rows.tsx exports components, and a .tsx exporting non-components
|
|
4
|
+
// breaks React Fast Refresh -- editing NAV_ITEMS would remount every settings
|
|
5
|
+
// row instead of hot-updating. `react-doctor/only-export-components` catches it.
|
|
6
|
+
// Three templates (settings, settings-sidebar, settings-dialog) read from here,
|
|
7
|
+
// so this data is genuinely shared rather than co-located by accident.
|
|
8
|
+
import type {CSSProperties} from 'react';
|
|
9
|
+
import type {LucideIcon} from 'lucide-react';
|
|
10
|
+
import {
|
|
11
|
+
Bell,
|
|
12
|
+
Briefcase,
|
|
13
|
+
CreditCard,
|
|
14
|
+
FileText,
|
|
15
|
+
Fingerprint,
|
|
16
|
+
Globe,
|
|
17
|
+
KeyRound,
|
|
18
|
+
Laptop,
|
|
19
|
+
Lock,
|
|
20
|
+
Share2,
|
|
21
|
+
ShieldCheck,
|
|
22
|
+
Smartphone,
|
|
23
|
+
SquarePen,
|
|
24
|
+
Tablet,
|
|
25
|
+
User,
|
|
26
|
+
UserCog,
|
|
27
|
+
} from 'lucide-react';
|
|
28
|
+
|
|
29
|
+
export interface SettingsNavItem {
|
|
30
|
+
label: string;
|
|
31
|
+
icon: LucideIcon;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const NAV_ITEMS: SettingsNavItem[] = [
|
|
35
|
+
{label: 'Personal information', icon: User},
|
|
36
|
+
{label: 'Login & security', icon: Lock},
|
|
37
|
+
{label: 'Privacy', icon: ShieldCheck},
|
|
38
|
+
{label: 'Notifications', icon: Bell},
|
|
39
|
+
{label: 'Taxes', icon: FileText},
|
|
40
|
+
{label: 'Payments', icon: CreditCard},
|
|
41
|
+
{label: 'Languages & currency', icon: Globe},
|
|
42
|
+
{label: 'Travel for work', icon: Briefcase},
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
export interface InfoRow {
|
|
46
|
+
label: string;
|
|
47
|
+
value: string;
|
|
48
|
+
action: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const LOGIN_ROWS: InfoRow[] = [
|
|
52
|
+
{label: 'Password', value: 'Not created', action: 'Create'},
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
export const SOCIAL_ROWS: InfoRow[] = [
|
|
56
|
+
{label: 'Google', value: 'Connected', action: 'Disconnect'},
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
export interface DeviceRow {
|
|
60
|
+
label: string;
|
|
61
|
+
isCurrent?: boolean;
|
|
62
|
+
location: string;
|
|
63
|
+
action?: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export const DEVICE_ROWS: DeviceRow[] = [
|
|
67
|
+
{
|
|
68
|
+
label: 'OS X 10.15.7 · Chrome',
|
|
69
|
+
isCurrent: true,
|
|
70
|
+
location: 'Brașov, Transylvania · March 30, 2026 at 19:31',
|
|
71
|
+
},
|
|
72
|
+
{label: 'Session', location: 'August 9, 2023 at 04:19', action: 'Log out'},
|
|
73
|
+
{
|
|
74
|
+
label: 'OS X 10.15.7 · unknown',
|
|
75
|
+
location: 'Whitby, England · April 14, 2023 at 17:47',
|
|
76
|
+
action: 'Log out',
|
|
77
|
+
},
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
export interface InfoTileData {
|
|
81
|
+
icon: LucideIcon;
|
|
82
|
+
title: string;
|
|
83
|
+
body: string;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export const INFO_TILES: InfoTileData[] = [
|
|
87
|
+
{
|
|
88
|
+
icon: Lock,
|
|
89
|
+
title: "Why isn't my info shown here?",
|
|
90
|
+
body: "We're veiling some crypt details to protect your identity.",
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
icon: SquarePen,
|
|
94
|
+
title: 'Which details can be edited?',
|
|
95
|
+
body: "Contact runes and personal details can be edited. If these were used to verify your identity, you'll need to be verified anew before your next stay, or to keep hosting your crypt.",
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
icon: Share2,
|
|
99
|
+
title: 'What info is shared with others?',
|
|
100
|
+
body: 'We only release contact runes after a booking is sealed.',
|
|
101
|
+
},
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
// Shared row chrome: the icon chip, nowrap action column, and sidebar heading
|
|
105
|
+
// offset duplicated across settings-dialog and settings-sidebar. Tokens only,
|
|
106
|
+
// no layout opinions — templates keep their own padding/dividers.
|
|
107
|
+
export const iconBox: CSSProperties = {
|
|
108
|
+
borderRadius: 'var(--radius-container)',
|
|
109
|
+
backgroundColor: 'var(--color-background-surface)',
|
|
110
|
+
flexShrink: 0,
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
// Keeps row actions ("Log out", "Deactivate") on one line: without this the
|
|
114
|
+
// action column wraps mid-phrase at tablet widths while the info column still
|
|
115
|
+
// has room to wrap instead.
|
|
116
|
+
export const actionNoWrap: CSSProperties = {
|
|
117
|
+
flexShrink: 0,
|
|
118
|
+
whiteSpace: 'nowrap',
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
// Aligns the sidebar heading with list-item label text. No heading margin prop.
|
|
122
|
+
export const sideNavHeading: CSSProperties = {
|
|
123
|
+
marginInline: 'var(--spacing-4)',
|
|
124
|
+
};
|
|
125
|
+
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
// Row RENDERERS for the settings dialog and settings sidebar templates.
|
|
2
|
+
// The data they render lives in shared/settings-data.ts, split out so this file
|
|
3
|
+
// exports only components -- a .tsx exporting non-components breaks React Fast
|
|
4
|
+
// Refresh, and three templates read that data, so it was never private to here.
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
DEVICE_ROWS,
|
|
8
|
+
INFO_TILES,
|
|
9
|
+
LOGIN_ROWS,
|
|
10
|
+
SOCIAL_ROWS,
|
|
11
|
+
actionNoWrap,
|
|
12
|
+
iconBox,
|
|
13
|
+
type DeviceRow,
|
|
14
|
+
type InfoRow,
|
|
15
|
+
type InfoTileData,
|
|
16
|
+
type SettingsNavItem,
|
|
17
|
+
} from 'astryx-dracula/shared/settings-data';
|
|
18
|
+
import {
|
|
19
|
+
Lock,
|
|
20
|
+
ShieldCheck,
|
|
21
|
+
Bell,
|
|
22
|
+
FileText,
|
|
23
|
+
CreditCard,
|
|
24
|
+
Globe,
|
|
25
|
+
Briefcase,
|
|
26
|
+
SquarePen,
|
|
27
|
+
Share2,
|
|
28
|
+
} from 'lucide-react';
|
|
29
|
+
import type {CSSProperties, MouseEvent, ReactNode} from 'react';
|
|
30
|
+
import {VStack, HStack} from '@astryxdesign/core/Layout';
|
|
31
|
+
import {Text} from '@astryxdesign/core/Text';
|
|
32
|
+
import {Link} from '@astryxdesign/core/Link';
|
|
33
|
+
import {Button} from '@astryxdesign/core/Button';
|
|
34
|
+
import {Divider} from '@astryxdesign/core/Divider';
|
|
35
|
+
|
|
36
|
+
// ─── Row components ──────────────────────────────────────────────────────────
|
|
37
|
+
// The row renderers themselves, moved here from both templates. The two copies
|
|
38
|
+
// were byte-identical apart from two things that genuinely differ per
|
|
39
|
+
// template, so those are props rather than forks: `href` (each template anchors
|
|
40
|
+
// its Links to its own route) and `style` (the sidebar pads its rows, the
|
|
41
|
+
// dialog does not). Verified by hash before the move — the row BODIES matched
|
|
42
|
+
// exactly; only these two call-site differences remained.
|
|
43
|
+
|
|
44
|
+
export function InfoRowItem({
|
|
45
|
+
label,
|
|
46
|
+
value,
|
|
47
|
+
action,
|
|
48
|
+
href,
|
|
49
|
+
style,
|
|
50
|
+
}: InfoRow & {href: string; style?: CSSProperties}) {
|
|
51
|
+
return (
|
|
52
|
+
<>
|
|
53
|
+
<HStack hAlign="between" vAlign="start" style={style}>
|
|
54
|
+
<VStack gap={0}>
|
|
55
|
+
<Text type="body" weight="semibold" display="block">
|
|
56
|
+
{label}
|
|
57
|
+
</Text>
|
|
58
|
+
<Text type="body" color="secondary" display="block">
|
|
59
|
+
{value}
|
|
60
|
+
</Text>
|
|
61
|
+
</VStack>
|
|
62
|
+
{action && (
|
|
63
|
+
<Link href={href} style={actionNoWrap}>
|
|
64
|
+
{action}
|
|
65
|
+
</Link>
|
|
66
|
+
)}
|
|
67
|
+
</HStack>
|
|
68
|
+
<Divider />
|
|
69
|
+
</>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function ExpandableRowEditing({
|
|
74
|
+
label,
|
|
75
|
+
children,
|
|
76
|
+
onCancel,
|
|
77
|
+
onSave,
|
|
78
|
+
style,
|
|
79
|
+
}: {
|
|
80
|
+
label: string;
|
|
81
|
+
children: ReactNode;
|
|
82
|
+
onCancel: () => void;
|
|
83
|
+
onSave: () => void;
|
|
84
|
+
style?: CSSProperties;
|
|
85
|
+
}) {
|
|
86
|
+
return (
|
|
87
|
+
<VStack gap={4} style={style}>
|
|
88
|
+
<Text type="body" weight="semibold" display="block">
|
|
89
|
+
{label}
|
|
90
|
+
</Text>
|
|
91
|
+
{children}
|
|
92
|
+
<HStack gap={2}>
|
|
93
|
+
<Button label="Save" variant="primary" onClick={onSave} />
|
|
94
|
+
<Button label="Cancel" variant="ghost" onClick={onCancel} />
|
|
95
|
+
</HStack>
|
|
96
|
+
</VStack>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function ExpandableRowViewing({
|
|
101
|
+
label,
|
|
102
|
+
value,
|
|
103
|
+
onEdit,
|
|
104
|
+
href,
|
|
105
|
+
style,
|
|
106
|
+
}: {
|
|
107
|
+
label: string;
|
|
108
|
+
value: string;
|
|
109
|
+
onEdit: () => void;
|
|
110
|
+
href: string;
|
|
111
|
+
style?: CSSProperties;
|
|
112
|
+
}) {
|
|
113
|
+
return (
|
|
114
|
+
<HStack hAlign="between" vAlign="start" style={style}>
|
|
115
|
+
<VStack gap={0}>
|
|
116
|
+
<Text type="body" weight="semibold" display="block">
|
|
117
|
+
{label}
|
|
118
|
+
</Text>
|
|
119
|
+
<Text type="body" color="secondary" display="block">
|
|
120
|
+
{value}
|
|
121
|
+
</Text>
|
|
122
|
+
</VStack>
|
|
123
|
+
<Link
|
|
124
|
+
href={href}
|
|
125
|
+
style={actionNoWrap}
|
|
126
|
+
onClick={(e: MouseEvent) => {
|
|
127
|
+
e.preventDefault();
|
|
128
|
+
onEdit();
|
|
129
|
+
}}>
|
|
130
|
+
Edit
|
|
131
|
+
</Link>
|
|
132
|
+
</HStack>
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function ExpandableRow({
|
|
137
|
+
label,
|
|
138
|
+
value,
|
|
139
|
+
children,
|
|
140
|
+
isExpanded,
|
|
141
|
+
onEdit,
|
|
142
|
+
onCancel,
|
|
143
|
+
onSave,
|
|
144
|
+
href,
|
|
145
|
+
style,
|
|
146
|
+
}: {
|
|
147
|
+
label: string;
|
|
148
|
+
value: string;
|
|
149
|
+
children: ReactNode;
|
|
150
|
+
isExpanded: boolean;
|
|
151
|
+
onEdit: () => void;
|
|
152
|
+
onCancel: () => void;
|
|
153
|
+
onSave: () => void;
|
|
154
|
+
href: string;
|
|
155
|
+
style?: CSSProperties;
|
|
156
|
+
}) {
|
|
157
|
+
return (
|
|
158
|
+
<>
|
|
159
|
+
{isExpanded ? (
|
|
160
|
+
<ExpandableRowEditing
|
|
161
|
+
label={label}
|
|
162
|
+
onCancel={onCancel}
|
|
163
|
+
onSave={onSave}
|
|
164
|
+
style={style}>
|
|
165
|
+
{children}
|
|
166
|
+
</ExpandableRowEditing>
|
|
167
|
+
) : (
|
|
168
|
+
<ExpandableRowViewing
|
|
169
|
+
label={label}
|
|
170
|
+
value={value}
|
|
171
|
+
onEdit={onEdit}
|
|
172
|
+
href={href}
|
|
173
|
+
style={style}
|
|
174
|
+
/>
|
|
175
|
+
)}
|
|
176
|
+
<Divider />
|
|
177
|
+
</>
|
|
178
|
+
);
|
|
179
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// Merged trend-bar sparkline: absorbs the dashboard max-normalized fixed
|
|
2
|
+
// geometry and the portfolio range-normalized step geometry behind one mode
|
|
3
|
+
// switch. Purple stays out of the chart so it keeps meaning "interactive".
|
|
4
|
+
|
|
5
|
+
import {CHART_HUES} from 'astryx-dracula/shared/chart-hues';
|
|
6
|
+
|
|
7
|
+
export interface SparkPoint {
|
|
8
|
+
id: string;
|
|
9
|
+
value: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface SparklineProps {
|
|
13
|
+
data: SparkPoint[];
|
|
14
|
+
/** REQUIRED and unique per instance: 4+ sparklines sharing one name is an
|
|
15
|
+
* a11y defect (the portfolio bug this module fixes). */
|
|
16
|
+
label: string;
|
|
17
|
+
positive: boolean;
|
|
18
|
+
/** max: fixed geometry normalized against the series max (dashboard tiles).
|
|
19
|
+
* range: step geometry normalized against the series range (market rows).
|
|
20
|
+
* @default 'max' */
|
|
21
|
+
mode?: 'max' | 'range';
|
|
22
|
+
/** @default false */
|
|
23
|
+
isCompact?: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function Sparkline({
|
|
27
|
+
data,
|
|
28
|
+
label,
|
|
29
|
+
positive,
|
|
30
|
+
mode = 'max',
|
|
31
|
+
isCompact = false,
|
|
32
|
+
}: SparklineProps) {
|
|
33
|
+
// Role tokens, not --dracula-* primitives. Byte-identical values
|
|
34
|
+
// (--color-data-categorical-green IS #50FA7B), so this is a vocabulary
|
|
35
|
+
// change with no visual delta -- it just means the sparkline agrees with
|
|
36
|
+
// chart-hues.ts instead of being the one chart module that decided alone.
|
|
37
|
+
// Reported by a consumer whose test asserted their sparkline fill equalled
|
|
38
|
+
// their CHART_HUES.green and stopped agreeing across a version bump.
|
|
39
|
+
const fill = positive ? CHART_HUES.green : CHART_HUES.red;
|
|
40
|
+
if (mode === 'range') {
|
|
41
|
+
const values = data.map(point => point.value);
|
|
42
|
+
const max = Math.max(...values);
|
|
43
|
+
const min = Math.min(...values);
|
|
44
|
+
const range = Math.max(1, max - min);
|
|
45
|
+
const step = 300 / data.length;
|
|
46
|
+
const height = isCompact ? 24 : 40;
|
|
47
|
+
const baseline = isCompact ? 21 : 36;
|
|
48
|
+
const plot = isCompact ? 18 : 32;
|
|
49
|
+
const floor = isCompact ? 2 : 3;
|
|
50
|
+
const barWidth = Math.max(1.5, step - 1);
|
|
51
|
+
return (
|
|
52
|
+
<svg
|
|
53
|
+
viewBox={`0 0 300 ${height}`}
|
|
54
|
+
width="100%"
|
|
55
|
+
height={height}
|
|
56
|
+
role="img"
|
|
57
|
+
aria-label={label}>
|
|
58
|
+
{data.map((point, slot) => {
|
|
59
|
+
const barHeight = Math.max(
|
|
60
|
+
floor,
|
|
61
|
+
((point.value - min) / range) * plot,
|
|
62
|
+
);
|
|
63
|
+
return (
|
|
64
|
+
<rect
|
|
65
|
+
key={point.id}
|
|
66
|
+
x={slot * step}
|
|
67
|
+
y={baseline - barHeight}
|
|
68
|
+
width={barWidth}
|
|
69
|
+
height={barHeight}
|
|
70
|
+
rx={Math.min(2, barWidth / 2)}
|
|
71
|
+
fill={fill}
|
|
72
|
+
/>
|
|
73
|
+
);
|
|
74
|
+
})}
|
|
75
|
+
</svg>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
const max = Math.max(...data.map(point => point.value));
|
|
79
|
+
return (
|
|
80
|
+
<svg
|
|
81
|
+
viewBox="0 0 300 40"
|
|
82
|
+
width="100%"
|
|
83
|
+
height={40}
|
|
84
|
+
role="img"
|
|
85
|
+
aria-label={label}>
|
|
86
|
+
{data.map((point, day) => {
|
|
87
|
+
const barHeight = Math.max(3, (point.value / max) * 32);
|
|
88
|
+
return (
|
|
89
|
+
<rect
|
|
90
|
+
key={point.id}
|
|
91
|
+
x={day * 10}
|
|
92
|
+
y={36 - barHeight}
|
|
93
|
+
width={7}
|
|
94
|
+
height={barHeight}
|
|
95
|
+
rx={3.5}
|
|
96
|
+
fill={fill}
|
|
97
|
+
/>
|
|
98
|
+
);
|
|
99
|
+
})}
|
|
100
|
+
</svg>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Brand sign-in marks shared by login-card and login-split (byte-identical
|
|
2
|
+
// Apple/Google SVGs). Bespoke inline glyphs, no icon-library equivalent.
|
|
3
|
+
import type {SVGProps} from 'react';
|
|
4
|
+
|
|
5
|
+
export const AppleIcon = (props: SVGProps<SVGSVGElement>) => (
|
|
6
|
+
<svg
|
|
7
|
+
viewBox="0 0 24 24"
|
|
8
|
+
fill="currentColor"
|
|
9
|
+
width={16}
|
|
10
|
+
height={16}
|
|
11
|
+
aria-hidden="true"
|
|
12
|
+
{...props}>
|
|
13
|
+
<path d="M17.05 20.28c-.98.95-2.05.88-3.08.4-1.09-.5-2.08-.48-3.24 0-1.44.62-2.2.44-3.06-.4C2.79 15.25 3.51 7.59 9.05 7.31c1.35.07 2.29.74 3.08.8 1.18-.24 2.31-.93 3.57-.84 1.51.12 2.65.72 3.4 1.8-3.12 1.87-2.38 5.98.48 7.13-.57 1.5-1.31 2.99-2.54 4.09zM12.03 7.25c-.15-2.23 1.66-4.07 3.74-4.25.29 2.58-2.34 4.5-3.74 4.25z" />
|
|
14
|
+
</svg>
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
export const GoogleIcon = (props: SVGProps<SVGSVGElement>) => (
|
|
18
|
+
<svg
|
|
19
|
+
viewBox="0 0 24 24"
|
|
20
|
+
fill="currentColor"
|
|
21
|
+
width={16}
|
|
22
|
+
height={16}
|
|
23
|
+
aria-hidden="true"
|
|
24
|
+
{...props}>
|
|
25
|
+
<path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92a5.06 5.06 0 01-2.2 3.32v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.1z" />
|
|
26
|
+
<path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" />
|
|
27
|
+
<path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" />
|
|
28
|
+
<path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" />
|
|
29
|
+
</svg>
|
|
30
|
+
);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
2
|
// XLE (canonical structure, validated with `bunx astryx layout check`):
|
|
3
|
-
// L > LC > V[g=8 a=center] > (V[g=1] > (H[g=2 a=center] > Ic + Tx
|
|
3
|
+
// L > LC > V[g=8 a=center] > (V[g=1] > (H[g=2 a=center] > Ic + Tx"Hi, Vlad"[t=body]) + Hd"Where should we start?"[level=1 type=display-2]) + ChC"Ask anything" + (V[g=6] > (TgG"Category" > Tg"Writing"*4) + Hd"Suggested prompts"[level=2] + (G[c={min:280} g=3] > (CC[p=4] > V[g=0.5] > Hd"Draft"[level=3] + Tx"Compose"[t=body])*4))
|
|
4
4
|
|
|
5
5
|
import {useRef, useState, type CSSProperties} from 'react';
|
|
6
6
|
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
} from '@astryxdesign/core/Typeahead';
|
|
23
23
|
import {ToggleButton, ToggleButtonGroup} from '@astryxdesign/core/ToggleButton';
|
|
24
24
|
import {Token} from '@astryxdesign/core/Token';
|
|
25
|
+
import {VisuallyHidden} from '@astryxdesign/core/VisuallyHidden';
|
|
25
26
|
import {ClickableCard} from '@astryxdesign/core/ClickableCard';
|
|
26
27
|
import {Grid} from '@astryxdesign/core/Grid';
|
|
27
28
|
import {Icon} from '@astryxdesign/core/Icon';
|
|
@@ -39,9 +40,17 @@ import {
|
|
|
39
40
|
} from 'lucide-react';
|
|
40
41
|
|
|
41
42
|
// Fill the content area so the greeting and composer stay vertically centered.
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
// `height`, not `minHeight`. A percentage min-height resolves against the
|
|
44
|
+
// containing block's DEFINITE height and computes to 0 when that is indefinite
|
|
45
|
+
// (CSS 2.1 10.5), so against a content-sized host it contributes nothing and
|
|
46
|
+
// the Layout's `height: 100%` still resolves to auto -- the edit is a no-op.
|
|
47
|
+
// Viewport units are absolute and need no ancestor, which is why every working
|
|
48
|
+
// instance in this repo uses them (editor.tsx:302, file-explorer.tsx:265,
|
|
49
|
+
// messaging-shell.tsx:67, ai-chat.tsx:64, kanban-board.tsx:704).
|
|
50
|
+
const pageStyle: CSSProperties = {height: '100dvh'};
|
|
51
|
+
// Five --spacing-4 steps: the box opens at ~80px so the empty composer reads
|
|
52
|
+
// as a writing surface, not a single-line field. No single token is 80px.
|
|
53
|
+
const composerInput: CSSProperties = {minHeight: 'calc(var(--spacing-4) * 5)'};
|
|
45
54
|
|
|
46
55
|
// Suggestion cards shown once a category is selected.
|
|
47
56
|
const CATEGORY_SUGGESTIONS: Record<
|
|
@@ -78,7 +87,7 @@ const CATEGORY_SUGGESTIONS: Record<
|
|
|
78
87
|
},
|
|
79
88
|
{
|
|
80
89
|
heading: 'Write a function',
|
|
81
|
-
body: '
|
|
90
|
+
body: 'Conjure a well-typed function warded against the dark',
|
|
82
91
|
prompt: 'Write a function that',
|
|
83
92
|
},
|
|
84
93
|
{
|
|
@@ -88,7 +97,7 @@ const CATEGORY_SUGGESTIONS: Record<
|
|
|
88
97
|
},
|
|
89
98
|
{
|
|
90
99
|
heading: 'Review my pull request',
|
|
91
|
-
body: '
|
|
100
|
+
body: 'Hunt bugs by moonlight, weigh contrast, bless the rite',
|
|
92
101
|
prompt: 'Review this code for bugs and improvements:',
|
|
93
102
|
},
|
|
94
103
|
],
|
|
@@ -100,7 +109,7 @@ const CATEGORY_SUGGESTIONS: Record<
|
|
|
100
109
|
},
|
|
101
110
|
{
|
|
102
111
|
heading: 'Explain a concept',
|
|
103
|
-
body: '
|
|
112
|
+
body: 'Unravel a tangled grimoire into plain night-tongue',
|
|
104
113
|
prompt: 'Explain the concept of',
|
|
105
114
|
},
|
|
106
115
|
{
|
|
@@ -122,12 +131,12 @@ const CATEGORY_SUGGESTIONS: Record<
|
|
|
122
131
|
},
|
|
123
132
|
{
|
|
124
133
|
heading: 'Write a dark tale',
|
|
125
|
-
body: '
|
|
134
|
+
body: 'Spin a moonlit tale crawling with night creatures',
|
|
126
135
|
prompt: 'Write a short story about',
|
|
127
136
|
},
|
|
128
137
|
{
|
|
129
138
|
heading: 'Design a concept',
|
|
130
|
-
body: '
|
|
139
|
+
body: 'Conjure nocturnal rites for product or visual haunts',
|
|
131
140
|
prompt: 'Help me design a concept for',
|
|
132
141
|
},
|
|
133
142
|
{
|
|
@@ -232,32 +241,45 @@ const commandTrigger: ChatComposerTrigger = {
|
|
|
232
241
|
|
|
233
242
|
const composerTriggers = [mentionTrigger, commandTrigger];
|
|
234
243
|
|
|
244
|
+
/** Composer attachment. The name is user-facing; the id is the React key, since
|
|
245
|
+
* two dropped files can share a name. */
|
|
246
|
+
interface Attachment {
|
|
247
|
+
id: string;
|
|
248
|
+
name: string;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const INITIAL_ATTACHMENTS: Attachment[] = [
|
|
252
|
+
'palette_brief.pdf',
|
|
253
|
+
'nocturne_v2.fig',
|
|
254
|
+
'api_spec.yaml',
|
|
255
|
+
'contrast_audit.csv',
|
|
256
|
+
'dracula_spec.pdf',
|
|
257
|
+
].map((name, index) => ({id: `file-${index + 1}`, name}));
|
|
258
|
+
|
|
259
|
+
// The composer's imperative insert methods mutate the DOM without emitting a
|
|
260
|
+
// change, so dispatch an input event to sync its value and clear the placeholder.
|
|
261
|
+
const syncComposerValue = () => {
|
|
262
|
+
document.activeElement?.dispatchEvent(new Event('input', {bubbles: true}));
|
|
263
|
+
};
|
|
264
|
+
|
|
235
265
|
// Main component
|
|
236
266
|
|
|
237
267
|
export default function AiChatLanding() {
|
|
238
268
|
const [mode, setMode] = useState<string | null>('auto');
|
|
239
269
|
const [category, setCategory] = useState<string | null>(null);
|
|
240
|
-
const [attachments, setAttachments] = useState<
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
'api_spec.yaml',
|
|
244
|
-
'contrast_audit.csv',
|
|
245
|
-
'dracula_spec.pdf',
|
|
246
|
-
]);
|
|
270
|
+
const [attachments, setAttachments] = useState<Attachment[]>(
|
|
271
|
+
INITIAL_ATTACHMENTS,
|
|
272
|
+
);
|
|
247
273
|
const [isModeMenuOpen, setIsModeMenuOpen] = useState(false);
|
|
248
274
|
const composerInputRef = useRef<ChatComposerInputHandle>(null);
|
|
249
275
|
const shouldFocusComposerRef = useRef(false);
|
|
276
|
+
// Two dropped files can share a name, so attachments carry their own id.
|
|
277
|
+
const nextAttachmentId = useRef(INITIAL_ATTACHMENTS.length);
|
|
250
278
|
const dictation = useChatDictation({inputRef: composerInputRef});
|
|
251
279
|
|
|
252
280
|
const activeMode = MODE_OPTIONS.find(m => m.key === mode) ?? MODE_OPTIONS[0];
|
|
253
281
|
const suggestions = category ? CATEGORY_SUGGESTIONS[category] : null;
|
|
254
282
|
|
|
255
|
-
// The composer's imperative insert methods mutate the DOM without emitting a
|
|
256
|
-
// change, so dispatch an input event to sync its value and clear the placeholder.
|
|
257
|
-
const syncComposerValue = () => {
|
|
258
|
-
document.activeElement?.dispatchEvent(new Event('input', {bubbles: true}));
|
|
259
|
-
};
|
|
260
|
-
|
|
261
283
|
const applySuggestion = (prompt: string) => {
|
|
262
284
|
const input = composerInputRef.current;
|
|
263
285
|
if (!input) {
|
|
@@ -311,8 +333,8 @@ export default function AiChatLanding() {
|
|
|
311
333
|
{/* Greeting */}
|
|
312
334
|
<VStack gap={1}>
|
|
313
335
|
<HStack gap={2} vAlign="center">
|
|
314
|
-
<Icon icon={Sparkles} size="md" color="
|
|
315
|
-
<Text type="
|
|
336
|
+
<Icon icon={Sparkles} size="md" color="secondary" />
|
|
337
|
+
<Text type="body">
|
|
316
338
|
Hi, Vlad
|
|
317
339
|
</Text>
|
|
318
340
|
</HStack>
|
|
@@ -331,19 +353,27 @@ export default function AiChatLanding() {
|
|
|
331
353
|
triggers={composerTriggers}
|
|
332
354
|
style={composerInput}
|
|
333
355
|
onFiles={files =>
|
|
334
|
-
setAttachments(prev => [
|
|
356
|
+
setAttachments(prev => [
|
|
357
|
+
...prev,
|
|
358
|
+
...files.map(file => ({
|
|
359
|
+
id: `file-${++nextAttachmentId.current}`,
|
|
360
|
+
name: file.name,
|
|
361
|
+
})),
|
|
362
|
+
])
|
|
335
363
|
}
|
|
336
364
|
/>
|
|
337
365
|
}
|
|
338
366
|
drawer={
|
|
339
367
|
attachments.length > 0 ? (
|
|
340
368
|
<ChatComposerDrawer count={attachments.length}>
|
|
341
|
-
{attachments.map(
|
|
369
|
+
{attachments.map(attachment => (
|
|
342
370
|
<Token
|
|
343
|
-
key={
|
|
344
|
-
label={name}
|
|
371
|
+
key={attachment.id}
|
|
372
|
+
label={attachment.name}
|
|
345
373
|
onRemove={() =>
|
|
346
|
-
setAttachments(prev =>
|
|
374
|
+
setAttachments(prev =>
|
|
375
|
+
prev.filter(item => item.id !== attachment.id),
|
|
376
|
+
)
|
|
347
377
|
}
|
|
348
378
|
/>
|
|
349
379
|
))}
|
|
@@ -431,7 +461,7 @@ export default function AiChatLanding() {
|
|
|
431
461
|
/>
|
|
432
462
|
|
|
433
463
|
{/* Category filters + suggestion cards */}
|
|
434
|
-
<VStack gap={6}
|
|
464
|
+
<VStack gap={6}>
|
|
435
465
|
<ToggleButtonGroup
|
|
436
466
|
label="Category"
|
|
437
467
|
value={category}
|
|
@@ -448,7 +478,9 @@ export default function AiChatLanding() {
|
|
|
448
478
|
</ToggleButtonGroup>
|
|
449
479
|
|
|
450
480
|
{suggestions && (
|
|
451
|
-
|
|
481
|
+
<>
|
|
482
|
+
<VisuallyHidden as="h2">Suggested prompts</VisuallyHidden>
|
|
483
|
+
<Grid columns={{minWidth: 280}} gap={3}>
|
|
452
484
|
{suggestions.map(suggestion => (
|
|
453
485
|
<ClickableCard
|
|
454
486
|
key={suggestion.heading}
|
|
@@ -460,14 +492,15 @@ export default function AiChatLanding() {
|
|
|
460
492
|
setMode(category);
|
|
461
493
|
}}>
|
|
462
494
|
<VStack gap={0.5}>
|
|
463
|
-
<Heading level={
|
|
495
|
+
<Heading level={3}>{suggestion.heading}</Heading>
|
|
464
496
|
<Text type="body" color="secondary">
|
|
465
497
|
{suggestion.body}
|
|
466
498
|
</Text>
|
|
467
499
|
</VStack>
|
|
468
500
|
</ClickableCard>
|
|
469
501
|
))}
|
|
470
|
-
|
|
502
|
+
</Grid>
|
|
503
|
+
</>
|
|
471
504
|
)}
|
|
472
505
|
</VStack>
|
|
473
506
|
</VStack>
|