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.
@@ -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
- width: pixel(44),
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
- width: proportional(1),
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
- width: pixel(72),
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(72),
700
+ width: pixel(112),
649
701
  },
650
702
  {
651
703
  key: 'assignee',
652
704
  header: 'Assignee',
653
- width: pixel(52),
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
- minSizePx: 280,
860
- maxSizePx: 500,
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
- <HStack gap={3} vAlign="center">
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
- {/* maxLines={1}: this is the worst case in the set — 18
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="truncate"
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 summary row at
992
- :751-757. */}
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" maxLines={1}>
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" maxLines={1}>
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
- <Avatar name={task.assignee} size="sm" />
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
@@ -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> data={data} columns={columns} idKey="id" hasHover />
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>