astryx-dracula 0.3.0 → 0.4.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/USAGE.md +27 -1
- package/astryx-dracula.d.ts +2 -2
- package/astryx-dracula.js +384 -3
- package/astryx-dracula.variants.d.ts +2 -14
- package/icons.mjs +125 -0
- package/package.json +16 -8
- package/shared/revenue-chart.tsx +72 -9
- package/shared/settings-data.ts +43 -1
- package/shared/settings-rows.tsx +81 -30
- package/shared/sparkline.tsx +9 -1
- package/templates/ai-chat-landing.tsx +17 -9
- package/templates/ai-chat.tsx +2 -2
- package/templates/centered-hero.tsx +2 -2
- package/templates/contact-form.tsx +11 -10
- package/templates/dashboard.tsx +7 -3
- package/templates/detail-page.tsx +11 -4
- package/templates/documentation-design.tsx +43 -29
- package/templates/documentation-technical.tsx +11 -9
- package/templates/file-explorer.tsx +82 -70
- package/templates/form-two-column.tsx +1 -1
- package/templates/ide.tsx +6 -6
- package/templates/incident-console.tsx +27 -4
- package/templates/login-card.tsx +8 -5
- package/templates/login-split.tsx +21 -12
- package/templates/login-sso.tsx +57 -41
- package/templates/login.tsx +23 -20
- package/templates/payment-form.tsx +39 -45
- package/templates/product-detail.tsx +4 -6
- package/templates/product-tour.tsx +2 -2
- package/templates/settings-dialog.tsx +84 -87
- package/templates/settings-sidebar.tsx +92 -80
- package/templates/table-grouped.tsx +126 -29
- package/templates/table.tsx +21 -2
- package/templates/tech-report.tsx +1 -1
- package/theme.css +211 -119
- package/tokens.css +15 -7
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
2
|
// XLE (canonical structure, validated with `bunx astryx layout check`):
|
|
3
|
-
// L > (LH[divider] > V[g=4] > (H[j=between a=center] > Hd"Night-shift issues"[level=1] + B.primary"Raise issue"[opens=#raise-issue]) + (H[g=2 a=center] > PS + Po > B.secondary"View options")) + (LC[p=0] > T[hover] > (TR > THC*7) + (TR > TC*7)*4) + (LP > V[g=4] > (H > Tx[t=supporting] + IB"Close panel") + (V[g=1] > Hd[level=2] + Tx[t=body]) + ML + (D + (V[g=2] > Tx"Labels"[t=label] + (H[g=2] > Tk*2)))) ;; Dlg#raise-issue > (DH"Raise issue" + (LC[p=4] > V[g=4] > TI*4) + (LF > H[j=end g=2] > B.secondary"Cancel" + B.primary"Raise issue"))
|
|
3
|
+
// L > (LH[divider] > V[g=4] > (H[j=between a=center] > Hd"Night-shift issues"[level=1] + B.primary"Raise issue"[opens=#raise-issue]) + (H[g=2 a=center] > PS + Po > B.secondary"View options")) + (LC[p=0] > (Tx"Scroll the table sideways to see every column"[t=supporting] + D) + T[hover] > (TR > THC*7) + (TR > TC*7)*4) + (LP > V[g=4] > (H > Tx[t=supporting] + IB"Close panel") + (V[g=1] > Hd[level=2] + Tx[t=body]) + ML + (D + (V[g=2] > Tx"Labels"[t=label] + (H[g=2] > Tk*2)))) ;; Dlg#raise-issue > (DH"Raise issue" + (LC[p=4] > V[g=4] > TI*4) + (LF > H[j=end g=2] > B.secondary"Cancel" + B.primary"Raise issue"))
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Table Grouped — the night-shift issue tracker: a grouped, collapsible issue table with a PowerSearch bar and a resizable detail inspector. (Frame/responsive/container: see XLE header above.)
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import React, {useState, useMemo} from 'react';
|
|
9
|
+
import React, {useRef, useState, useMemo} from 'react';
|
|
10
10
|
import {useResizable, ResizeHandle} from '@astryxdesign/core/Resizable';
|
|
11
11
|
import type {ResizableProps} from '@astryxdesign/core/Resizable';
|
|
12
12
|
import {
|
|
@@ -41,6 +41,7 @@ import {
|
|
|
41
41
|
Table,
|
|
42
42
|
TableRow,
|
|
43
43
|
TableCell,
|
|
44
|
+
TableHeaderCell,
|
|
44
45
|
proportional,
|
|
45
46
|
pixel,
|
|
46
47
|
resolveColumnWidths,
|
|
@@ -70,6 +71,37 @@ const groupHeaderCell: React.CSSProperties = {
|
|
|
70
71
|
padding: 'var(--spacing-3) var(--spacing-4)',
|
|
71
72
|
};
|
|
72
73
|
|
|
74
|
+
// Chrome above the scroller, never an overlay on it: the Actions menu is the
|
|
75
|
+
// last column inside the overflow, and a sticky element covering row controls
|
|
76
|
+
// is a defect (SKILL.md, responsive rules).
|
|
77
|
+
const scrollCueBar: React.CSSProperties = {
|
|
78
|
+
padding: 'var(--spacing-2) var(--spacing-4)',
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// Whether the table currently overflows its scroller. Measured rather than
|
|
82
|
+
// derived from a breakpoint: the pane narrows both on window resize and when
|
|
83
|
+
// the (resizable) inspector opens, and the scroller is core's own, so a media
|
|
84
|
+
// query would disagree with what the reader actually sees. This is the
|
|
85
|
+
// scroller's own overflow test, so the hint can never contradict the scroll.
|
|
86
|
+
function useTableOverflows(paneRef: React.RefObject<HTMLDivElement | null>) {
|
|
87
|
+
const [overflows, setOverflows] = useState(false);
|
|
88
|
+
React.useLayoutEffect(() => {
|
|
89
|
+
// The <table>'s parent is the scroll container core renders around it.
|
|
90
|
+
const scroller = paneRef.current?.querySelector('table')?.parentElement;
|
|
91
|
+
if (!scroller) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const measure = () => {
|
|
95
|
+
setOverflows(scroller.scrollWidth > scroller.clientWidth + 1);
|
|
96
|
+
};
|
|
97
|
+
measure();
|
|
98
|
+
const observer = new ResizeObserver(measure);
|
|
99
|
+
observer.observe(scroller);
|
|
100
|
+
return () => observer.disconnect();
|
|
101
|
+
}, [paneRef]);
|
|
102
|
+
return overflows;
|
|
103
|
+
}
|
|
104
|
+
|
|
73
105
|
// Types
|
|
74
106
|
type TaskStatus = 'in_progress' | 'todo' | 'backlog' | 'done';
|
|
75
107
|
type TaskPriority = 'urgent' | 'high' | 'medium' | 'low' | 'none';
|
|
@@ -621,36 +653,61 @@ function getGroupLabel(groupBy: GroupByField, key: string): string {
|
|
|
621
653
|
return key;
|
|
622
654
|
}
|
|
623
655
|
|
|
656
|
+
// Real widths, because `resolveColumnWidths` derives the table's aggregate
|
|
657
|
+
// min-width from exactly these numbers: every `pixel()` here also sets
|
|
658
|
+
// min-width (the table may not shrink a fixed column) and every
|
|
659
|
+
// `proportional()` contributes its own min. Set too small, the core honors the
|
|
660
|
+
// number anyway and clips the cell — that is how a 44px status column came to
|
|
661
|
+
// hold "In Progress" (91px of text) and a 72px date column came to hold a
|
|
662
|
+
// header plus a value. The floors below are the measured width of the widest
|
|
663
|
+
// real value in the column plus the 24px of balanced-density cell padding, so
|
|
664
|
+
// no value in the data is ever narrower than its own content.
|
|
665
|
+
// The consequence is an honest min-width of 1014px, which is why the table
|
|
666
|
+
// scrolls in its own container below that and why the cue below it exists:
|
|
667
|
+
// at 390 the status and the Issue column both land on screen, and the
|
|
668
|
+
// remaining columns are one swipe away.
|
|
624
669
|
const columns: TableColumn<TaskRow>[] = [
|
|
625
670
|
{
|
|
626
671
|
key: 'status',
|
|
627
672
|
header: '',
|
|
628
|
-
|
|
673
|
+
// StatusDot + the status word, which WCAG 1.4.1 requires as a channel
|
|
674
|
+
// beside the hue.
|
|
675
|
+
width: pixel(116),
|
|
629
676
|
},
|
|
630
677
|
{
|
|
631
678
|
key: 'title',
|
|
632
679
|
header: 'Issue',
|
|
633
|
-
|
|
680
|
+
// 2fr of the flexible pair: the issue id and title are what the reader
|
|
681
|
+
// scans the list for.
|
|
682
|
+
width: proportional(2, {minWidth: 280}),
|
|
634
683
|
},
|
|
635
684
|
{
|
|
636
685
|
key: 'project',
|
|
637
686
|
header: 'Project',
|
|
638
|
-
width: proportional(1),
|
|
687
|
+
width: proportional(1, {minWidth: 150}),
|
|
639
688
|
},
|
|
640
689
|
{
|
|
641
690
|
key: 'created',
|
|
642
691
|
header: 'Created',
|
|
643
|
-
|
|
692
|
+
// 112, not 80: the header label measures 83px at label-semibold, and a
|
|
693
|
+
// header cell always truncates, so the floor has to clear the label and
|
|
694
|
+
// not just the "Jul 3" value underneath it.
|
|
695
|
+
width: pixel(112),
|
|
644
696
|
},
|
|
645
697
|
{
|
|
646
698
|
key: 'updated',
|
|
647
699
|
header: 'Updated',
|
|
648
|
-
width: pixel(
|
|
700
|
+
width: pixel(112),
|
|
649
701
|
},
|
|
650
702
|
{
|
|
651
703
|
key: 'assignee',
|
|
652
704
|
header: 'Assignee',
|
|
653
|
-
|
|
705
|
+
// Avatar AND name. At 52px the column showed the avatar alone, which
|
|
706
|
+
// left the name reachable only in the inspector — and the inspector is
|
|
707
|
+
// gone below 1024. 168 clears the widest name in the data
|
|
708
|
+
// ("Isabella Nguyen", 108px of text) beside the 24px avatar, so it never
|
|
709
|
+
// breaks across two lines.
|
|
710
|
+
width: pixel(168),
|
|
654
711
|
},
|
|
655
712
|
{
|
|
656
713
|
key: 'actions',
|
|
@@ -856,13 +913,17 @@ export default function TableGrouped() {
|
|
|
856
913
|
|
|
857
914
|
const detailPanel = useResizable({
|
|
858
915
|
defaultSize: 360,
|
|
859
|
-
|
|
860
|
-
|
|
916
|
+
minSize: 280,
|
|
917
|
+
maxSize: 500,
|
|
861
918
|
});
|
|
862
919
|
|
|
863
920
|
const COL_COUNT = columns.length;
|
|
864
921
|
const resolvedWidths = resolveColumnWidths(columns);
|
|
865
922
|
|
|
923
|
+
// The pane the table scrolls inside, and whether it currently needs to.
|
|
924
|
+
const paneRef = useRef<HTMLDivElement | null>(null);
|
|
925
|
+
const tableOverflows = useTableOverflows(paneRef);
|
|
926
|
+
|
|
866
927
|
return (
|
|
867
928
|
<>
|
|
868
929
|
<Layout
|
|
@@ -870,15 +931,16 @@ export default function TableGrouped() {
|
|
|
870
931
|
header={
|
|
871
932
|
<LayoutHeader hasDivider padding={6}>
|
|
872
933
|
<VStack gap={4}>
|
|
873
|
-
|
|
934
|
+
{/* wrap: below ~460px the Raise issue button drops to its own
|
|
935
|
+
line instead of starving the heading, so the display-2 title
|
|
936
|
+
keeps its full measure. The earlier maxLines={1} clipped it
|
|
937
|
+
to "Night-shift i…" at 390, and its reveal path was a
|
|
938
|
+
Tooltip — a hover affordance a phone never fires. Wrapping
|
|
939
|
+
breaks on the space between the two words, so no value is
|
|
940
|
+
lost and none is sliced mid-word (SC 1.4.10). */}
|
|
941
|
+
<HStack gap={3} vAlign="center" wrap="wrap">
|
|
874
942
|
<StackItem size="fill">
|
|
875
|
-
|
|
876
|
-
characters at 35px is 378px of a 342px header column, so
|
|
877
|
-
it wraps to THREE lines and the header measures 180px
|
|
878
|
-
(21.3% of a 390x844 viewport). Core wires maxLines to
|
|
879
|
-
useTruncation, so the full title stays reachable in a
|
|
880
|
-
Tooltip (SC 1.4.10). */}
|
|
881
|
-
<Heading level={1} type="display-2" maxLines={1}>Night-shift issues</Heading>
|
|
943
|
+
<Heading level={1} type="display-2">Night-shift issues</Heading>
|
|
882
944
|
</StackItem>
|
|
883
945
|
<Button
|
|
884
946
|
label="Raise issue"
|
|
@@ -924,12 +986,27 @@ export default function TableGrouped() {
|
|
|
924
986
|
</LayoutHeader>
|
|
925
987
|
}
|
|
926
988
|
content={
|
|
927
|
-
<LayoutContent role="main" padding={0}>
|
|
989
|
+
<LayoutContent role="main" padding={0} ref={paneRef}>
|
|
990
|
+
{/* The visible cue for the scroller core already owns: the table
|
|
991
|
+
carries a real min-width, so below it the columns past Issue
|
|
992
|
+
live off-screen. Appears only while that is true — measured,
|
|
993
|
+
not assumed — so it is never a stale promise at 1440. */}
|
|
994
|
+
{tableOverflows && (
|
|
995
|
+
<>
|
|
996
|
+
<HStack gap={2} vAlign="center" style={scrollCueBar}>
|
|
997
|
+
<Icon icon={ChevronRight} size="sm" color="secondary" />
|
|
998
|
+
<Text type="supporting" color="secondary">
|
|
999
|
+
Scroll the table sideways to see every column
|
|
1000
|
+
</Text>
|
|
1001
|
+
</HStack>
|
|
1002
|
+
<Divider />
|
|
1003
|
+
</>
|
|
1004
|
+
)}
|
|
928
1005
|
<Table
|
|
929
1006
|
columns={columns}
|
|
930
1007
|
density="balanced"
|
|
931
1008
|
dividers="rows"
|
|
932
|
-
textOverflow="
|
|
1009
|
+
textOverflow="wrap"
|
|
933
1010
|
hasHover>
|
|
934
1011
|
<colgroup>
|
|
935
1012
|
{columns.map(col => (
|
|
@@ -939,6 +1016,19 @@ export default function TableGrouped() {
|
|
|
939
1016
|
/>
|
|
940
1017
|
))}
|
|
941
1018
|
</colgroup>
|
|
1019
|
+
{/* A scroller needs its columns named: once the reader has
|
|
1020
|
+
scrolled sideways at 390, this row is the only thing that
|
|
1021
|
+
says which value they are looking at. Rendered from
|
|
1022
|
+
`columns` so the labels and the widths cannot drift, and as
|
|
1023
|
+
real <th scope="col"> cells so the association reaches
|
|
1024
|
+
assistive tech. */}
|
|
1025
|
+
<TableRow>
|
|
1026
|
+
{columns.map(col => (
|
|
1027
|
+
<TableHeaderCell key={col.key} scope="col">
|
|
1028
|
+
{col.header}
|
|
1029
|
+
</TableHeaderCell>
|
|
1030
|
+
))}
|
|
1031
|
+
</TableRow>
|
|
942
1032
|
{groupKeys.map(key => {
|
|
943
1033
|
const tasks = grouped.get(key);
|
|
944
1034
|
if (!tasks || tasks.length === 0) {
|
|
@@ -988,8 +1078,8 @@ export default function TableGrouped() {
|
|
|
988
1078
|
{/* StatusDot paints nothing but an 8px dot — its
|
|
989
1079
|
label is aria-label on a role="img" span — so
|
|
990
1080
|
four states here read as hue alone. Same
|
|
991
|
-
dot-plus-word pairing as the
|
|
992
|
-
|
|
1081
|
+
dot-plus-word pairing as the Status entry in
|
|
1082
|
+
TaskDetailPanel's MetadataList. */}
|
|
993
1083
|
<HStack gap={1} vAlign="center">
|
|
994
1084
|
<StatusDot
|
|
995
1085
|
variant={STATUS_DOT_VARIANT[task.status]}
|
|
@@ -1012,7 +1102,7 @@ export default function TableGrouped() {
|
|
|
1012
1102
|
{task.taskId}
|
|
1013
1103
|
</Text>
|
|
1014
1104
|
<StackItem size="fill">
|
|
1015
|
-
<Text type="body"
|
|
1105
|
+
<Text type="body">
|
|
1016
1106
|
{task.title}
|
|
1017
1107
|
{task.subtitle && (
|
|
1018
1108
|
<Text type="inherit" color="secondary">
|
|
@@ -1026,7 +1116,7 @@ export default function TableGrouped() {
|
|
|
1026
1116
|
</TableCell>
|
|
1027
1117
|
<TableCell>
|
|
1028
1118
|
{task.project ? (
|
|
1029
|
-
<Text type="body"
|
|
1119
|
+
<Text type="body">
|
|
1030
1120
|
{task.project}
|
|
1031
1121
|
</Text>
|
|
1032
1122
|
) : (
|
|
@@ -1038,21 +1128,28 @@ export default function TableGrouped() {
|
|
|
1038
1128
|
<TableCell>
|
|
1039
1129
|
<Text
|
|
1040
1130
|
type="supporting"
|
|
1041
|
-
color="secondary"
|
|
1042
|
-
maxLines={1}>
|
|
1131
|
+
color="secondary">
|
|
1043
1132
|
{task.created}
|
|
1044
1133
|
</Text>
|
|
1045
1134
|
</TableCell>
|
|
1046
1135
|
<TableCell>
|
|
1047
1136
|
<Text
|
|
1048
1137
|
type="supporting"
|
|
1049
|
-
color="secondary"
|
|
1050
|
-
maxLines={1}>
|
|
1138
|
+
color="secondary">
|
|
1051
1139
|
{task.updated}
|
|
1052
1140
|
</Text>
|
|
1053
1141
|
</TableCell>
|
|
1054
1142
|
<TableCell>
|
|
1055
|
-
|
|
1143
|
+
{/* Name beside the avatar: below 1024 the
|
|
1144
|
+
inspector is gone, so the avatar alone would
|
|
1145
|
+
leave the assignee readable by screen reader
|
|
1146
|
+
but not by eye. */}
|
|
1147
|
+
<HStack gap={2} vAlign="center">
|
|
1148
|
+
<Avatar name={task.assignee} size="sm" />
|
|
1149
|
+
<Text type="supporting" color="secondary">
|
|
1150
|
+
{task.assignee}
|
|
1151
|
+
</Text>
|
|
1152
|
+
</HStack>
|
|
1056
1153
|
</TableCell>
|
|
1057
1154
|
<TableCell>
|
|
1058
1155
|
<DropdownMenu
|
package/templates/table.tsx
CHANGED
|
@@ -5,9 +5,18 @@
|
|
|
5
5
|
import {Layout, LayoutHeader, LayoutContent, HStack} from '@astryxdesign/core/Layout';
|
|
6
6
|
import {Text, Heading} from '@astryxdesign/core/Text';
|
|
7
7
|
import {Button} from '@astryxdesign/core/Button';
|
|
8
|
-
import {Table} from '@astryxdesign/core/Table';
|
|
8
|
+
import {Table, pixel} from '@astryxdesign/core/Table';
|
|
9
9
|
import {StatusDot} from '@astryxdesign/core/StatusDot';
|
|
10
10
|
import type {TableColumn} from '@astryxdesign/core/Table';
|
|
11
|
+
import type {CSSProperties} from 'react';
|
|
12
|
+
|
|
13
|
+
// Each column below declares the width its own content needs, so the table
|
|
14
|
+
// takes that sum instead of splitting the viewport four ways and leaving a
|
|
15
|
+
// quarter of the page empty after a ten-character name and 290px of empty
|
|
16
|
+
// table to the right of an Edit button. `width: auto` is what lets the pixel
|
|
17
|
+
// widths survive — under the default `width: 100%` the fixed table layout
|
|
18
|
+
// hands the leftover space back to every column and the voids return.
|
|
19
|
+
const tableHugsContent: CSSProperties = {width: 'auto'};
|
|
11
20
|
|
|
12
21
|
type Relic = {
|
|
13
22
|
id: string;
|
|
@@ -36,6 +45,7 @@ const columns: TableColumn<Relic>[] = [
|
|
|
36
45
|
{
|
|
37
46
|
key: 'name',
|
|
38
47
|
header: 'Name',
|
|
48
|
+
width: pixel(144),
|
|
39
49
|
renderCell: (item: Relic) => (
|
|
40
50
|
<Text type="body" weight="semibold">
|
|
41
51
|
{item.name}
|
|
@@ -45,6 +55,7 @@ const columns: TableColumn<Relic>[] = [
|
|
|
45
55
|
{
|
|
46
56
|
key: 'status',
|
|
47
57
|
header: 'Status',
|
|
58
|
+
width: pixel(112),
|
|
48
59
|
renderCell: (item: Relic) => (
|
|
49
60
|
<HStack gap={1} vAlign="center">
|
|
50
61
|
<StatusDot
|
|
@@ -60,6 +71,7 @@ const columns: TableColumn<Relic>[] = [
|
|
|
60
71
|
{
|
|
61
72
|
key: 'updatedAt',
|
|
62
73
|
header: 'Updated',
|
|
74
|
+
width: pixel(112),
|
|
63
75
|
renderCell: (item: Relic) => (
|
|
64
76
|
<Text type="body" color="secondary" hasTabularNumbers>
|
|
65
77
|
{item.updatedAt}
|
|
@@ -69,6 +81,7 @@ const columns: TableColumn<Relic>[] = [
|
|
|
69
81
|
{
|
|
70
82
|
key: 'actions',
|
|
71
83
|
header: 'Actions',
|
|
84
|
+
width: pixel(96),
|
|
72
85
|
renderCell: (item: Relic) => (
|
|
73
86
|
<Button label={`Edit ${item.name}`} variant="secondary" size="sm">
|
|
74
87
|
Edit
|
|
@@ -93,7 +106,13 @@ export default function SimpleTable() {
|
|
|
93
106
|
}
|
|
94
107
|
content={
|
|
95
108
|
<LayoutContent>
|
|
96
|
-
<Table<Relic>
|
|
109
|
+
<Table<Relic>
|
|
110
|
+
data={data}
|
|
111
|
+
columns={columns}
|
|
112
|
+
idKey="id"
|
|
113
|
+
hasHover
|
|
114
|
+
style={tableHugsContent}
|
|
115
|
+
/>
|
|
97
116
|
</LayoutContent>
|
|
98
117
|
}
|
|
99
118
|
/>
|
|
@@ -1476,7 +1476,7 @@ export default function TechReport() {
|
|
|
1476
1476
|
footer={
|
|
1477
1477
|
<Text type="supporting" color="secondary">
|
|
1478
1478
|
Checkpoints and the full report:{' '}
|
|
1479
|
-
<Link href={REPORT_URL} target="_blank" type="supporting">
|
|
1479
|
+
<Link href={REPORT_URL} target="_blank" type="supporting" hasUnderline>
|
|
1480
1480
|
huggingface.co/deepseek-ai/DeepSeek-V4.1-Flash
|
|
1481
1481
|
</Link>
|
|
1482
1482
|
</Text>
|