@gcsa/console-core 0.1.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.
Files changed (85) hide show
  1. package/dist/gcsaChat.d.ts +6 -0
  2. package/dist/gcsaChat.d.ts.map +1 -0
  3. package/dist/gcsaChat.js +31 -0
  4. package/dist/gcsaClipboard.d.ts +2 -0
  5. package/dist/gcsaClipboard.d.ts.map +1 -0
  6. package/dist/gcsaClipboard.js +30 -0
  7. package/dist/gcsaCssLength.d.ts +3 -0
  8. package/dist/gcsaCssLength.d.ts.map +1 -0
  9. package/dist/gcsaCssLength.js +6 -0
  10. package/dist/gcsaDate.d.ts +15 -0
  11. package/dist/gcsaDate.d.ts.map +1 -0
  12. package/dist/gcsaDate.js +12 -0
  13. package/dist/gcsaDateTimeValue.d.ts +8 -0
  14. package/dist/gcsaDateTimeValue.d.ts.map +1 -0
  15. package/dist/gcsaDateTimeValue.js +16 -0
  16. package/dist/gcsaLocale.d.ts +109 -0
  17. package/dist/gcsaLocale.d.ts.map +1 -0
  18. package/dist/gcsaLocale.js +320 -0
  19. package/dist/gcsaMarkdown.d.ts +2 -0
  20. package/dist/gcsaMarkdown.d.ts.map +1 -0
  21. package/dist/gcsaMarkdown.js +19 -0
  22. package/dist/gcsaMenu.d.ts +21 -0
  23. package/dist/gcsaMenu.d.ts.map +1 -0
  24. package/dist/gcsaMenu.js +20 -0
  25. package/dist/gcsaMessage.d.ts +3 -0
  26. package/dist/gcsaMessage.d.ts.map +1 -0
  27. package/dist/gcsaMessage.js +1 -0
  28. package/dist/gcsaPagination.d.ts +13 -0
  29. package/dist/gcsaPagination.d.ts.map +1 -0
  30. package/dist/gcsaPagination.js +52 -0
  31. package/dist/gcsaPopupCollision.d.ts +22 -0
  32. package/dist/gcsaPopupCollision.d.ts.map +1 -0
  33. package/dist/gcsaPopupCollision.js +66 -0
  34. package/dist/gcsaPopupPositioning.d.ts +30 -0
  35. package/dist/gcsaPopupPositioning.d.ts.map +1 -0
  36. package/dist/gcsaPopupPositioning.js +18 -0
  37. package/dist/gcsaPopupStyles.d.ts +51 -0
  38. package/dist/gcsaPopupStyles.d.ts.map +1 -0
  39. package/dist/gcsaPopupStyles.js +115 -0
  40. package/dist/gcsaSize.d.ts +9 -0
  41. package/dist/gcsaSize.d.ts.map +1 -0
  42. package/dist/gcsaSize.js +13 -0
  43. package/dist/gcsaTableEvents.d.ts +27 -0
  44. package/dist/gcsaTableEvents.d.ts.map +1 -0
  45. package/dist/gcsaTableEvents.js +13 -0
  46. package/dist/gcsaTableLayout.d.ts +23 -0
  47. package/dist/gcsaTableLayout.d.ts.map +1 -0
  48. package/dist/gcsaTableLayout.js +36 -0
  49. package/dist/gcsaTableMigration.d.ts +34 -0
  50. package/dist/gcsaTableMigration.d.ts.map +1 -0
  51. package/dist/gcsaTableMigration.js +65 -0
  52. package/dist/gcsaTablePreferences.d.ts +16 -0
  53. package/dist/gcsaTablePreferences.d.ts.map +1 -0
  54. package/dist/gcsaTablePreferences.js +53 -0
  55. package/dist/gcsaTableRows.d.ts +8 -0
  56. package/dist/gcsaTableRows.d.ts.map +1 -0
  57. package/dist/gcsaTableRows.js +29 -0
  58. package/dist/gcsaTime.d.ts +23 -0
  59. package/dist/gcsaTime.d.ts.map +1 -0
  60. package/dist/gcsaTime.js +63 -0
  61. package/dist/index.d.ts +163 -0
  62. package/dist/index.d.ts.map +1 -0
  63. package/dist/index.js +30 -0
  64. package/package.json +37 -0
  65. package/src/gcsaChat.ts +35 -0
  66. package/src/gcsaClipboard.ts +28 -0
  67. package/src/gcsaCssLength.ts +5 -0
  68. package/src/gcsaDate.ts +30 -0
  69. package/src/gcsaDateTimeValue.ts +18 -0
  70. package/src/gcsaLocale.ts +431 -0
  71. package/src/gcsaMarkdown.ts +19 -0
  72. package/src/gcsaMenu.ts +43 -0
  73. package/src/gcsaMessage.ts +3 -0
  74. package/src/gcsaPagination.ts +68 -0
  75. package/src/gcsaPopupCollision.ts +111 -0
  76. package/src/gcsaPopupPositioning.ts +57 -0
  77. package/src/gcsaPopupStyles.ts +137 -0
  78. package/src/gcsaSize.ts +29 -0
  79. package/src/gcsaTableEvents.ts +36 -0
  80. package/src/gcsaTableLayout.ts +69 -0
  81. package/src/gcsaTableMigration.ts +104 -0
  82. package/src/gcsaTablePreferences.ts +61 -0
  83. package/src/gcsaTableRows.ts +36 -0
  84. package/src/gcsaTime.ts +83 -0
  85. package/src/index.ts +237 -0
@@ -0,0 +1,57 @@
1
+ /** Ark UI / Zag Popper 通用方位(与 Floating UI placement 对齐) */
2
+ export type GcsaPopupPlacement =
3
+ | 'top'
4
+ | 'top-start'
5
+ | 'top-end'
6
+ | 'right'
7
+ | 'right-start'
8
+ | 'right-end'
9
+ | 'bottom'
10
+ | 'bottom-start'
11
+ | 'bottom-end'
12
+ | 'left'
13
+ | 'left-start'
14
+ | 'left-end';
15
+
16
+ export type GcsaPopupPositioningOptions = {
17
+ placement?: GcsaPopupPlacement;
18
+ gutter?: number;
19
+ strategy?: 'fixed' | 'absolute';
20
+ sameWidth?: boolean;
21
+ fitViewport?: boolean;
22
+ flip?: boolean;
23
+ slide?: boolean;
24
+ /** zag 用 overlap 开启横向 shift,避免贴边触发器时面板溢出视口 */
25
+ overlap?: boolean;
26
+ overflowPadding?: number;
27
+ };
28
+
29
+ /**
30
+ * 统一浮层定位:flip / slide / overlap 默认开启,减少贴边溢出。
31
+ * 供 Select、Menu、Popover、Tooltip、DatePicker 等复用。
32
+ */
33
+ export function createGcsaPopupPositioning(options: GcsaPopupPositioningOptions = {}) {
34
+ const {
35
+ placement = 'bottom-start',
36
+ gutter = 8,
37
+ strategy = 'fixed',
38
+ sameWidth,
39
+ fitViewport,
40
+ flip = true,
41
+ slide = true,
42
+ overlap = true,
43
+ overflowPadding = 8
44
+ } = options;
45
+
46
+ return {
47
+ placement,
48
+ gutter,
49
+ strategy,
50
+ flip,
51
+ slide,
52
+ overlap,
53
+ overflowPadding,
54
+ ...(sameWidth === undefined ? {} : { sameWidth }),
55
+ ...(fitViewport === undefined ? {} : { fitViewport })
56
+ };
57
+ }
@@ -0,0 +1,137 @@
1
+ /**
2
+ * 浮层样式契约:与 packages/styles/src/console-ui.css 保持同步。
3
+ * 测试会校验 CSS 中是否仍包含这些规则,防止 padding / 碰撞变量被改坏。
4
+ */
5
+ export const GCSA_POPUP_OVERFLOW_PADDING = 8;
6
+ export const GCSA_POPUP_VIEWPORT_CSS_INSET = 16;
7
+
8
+ export const GCSA_POPUP_STYLE_CONTRACT = {
9
+ selectContent: {
10
+ selector: '.gcsa-select-content',
11
+ maxWidth: 'var(--available-width, 100vw)',
12
+ minWidth: 'min(160px, var(--reference-width, 160px))'
13
+ },
14
+ selectList: {
15
+ selector: '.gcsa-select-list',
16
+ padding: '4px'
17
+ },
18
+ selectItem: {
19
+ selector: '.gcsa-select-item',
20
+ padding: '8px 10px'
21
+ },
22
+ tooltip: {
23
+ selector: '.gcsa-tooltip',
24
+ padding: '6px 12px',
25
+ maxWidth: 'min(240px, var(--available-width, calc(100vw - 16px)))'
26
+ },
27
+ popover: {
28
+ selector: '.gcsa-popover',
29
+ padding: '12px 14px',
30
+ maxWidth: 'min(320px, var(--available-width, calc(100vw - 16px)))'
31
+ },
32
+ dropdown: {
33
+ selector: '.gcsa-dropdown',
34
+ maxWidth: 'var(--available-width, calc(100vw - 16px))'
35
+ },
36
+ dateTimePicker: {
37
+ selector: '.gcsa-date-picker__content.gcsa-datetime-picker__content',
38
+ maxWidth: 'var(--available-width, calc(100vw - 16px))'
39
+ }
40
+ } as const;
41
+
42
+ export type GcsaPopupStyleRule = {
43
+ selector: string;
44
+ padding?: string;
45
+ minWidth?: string;
46
+ maxWidth?: string;
47
+ };
48
+
49
+ export function stripCssComments(css: string): string {
50
+ return css.replace(/\/\*[\s\S]*?\*\//g, '');
51
+ }
52
+
53
+ export function extractCssRuleBlocks(css: string, selector: string): string[] {
54
+ const normalized = stripCssComments(css);
55
+ const blocks: string[] = [];
56
+ let depth = 0;
57
+ let index = 0;
58
+
59
+ while (index < normalized.length) {
60
+ const char = normalized[index];
61
+
62
+ if (char === '{') {
63
+ depth += 1;
64
+ index += 1;
65
+ continue;
66
+ }
67
+
68
+ if (char === '}') {
69
+ depth = Math.max(0, depth - 1);
70
+ index += 1;
71
+ continue;
72
+ }
73
+
74
+ if (depth !== 0 || char === '@') {
75
+ index += 1;
76
+ continue;
77
+ }
78
+
79
+ const braceStart = normalized.indexOf('{', index);
80
+ if (braceStart === -1) {
81
+ break;
82
+ }
83
+
84
+ const selectorPart = normalized.slice(index, braceStart).trim();
85
+ let ruleDepth = 1;
86
+ let cursor = braceStart + 1;
87
+ while (cursor < normalized.length && ruleDepth > 0) {
88
+ if (normalized[cursor] === '{') {
89
+ ruleDepth += 1;
90
+ }
91
+ if (normalized[cursor] === '}') {
92
+ ruleDepth -= 1;
93
+ }
94
+ cursor += 1;
95
+ }
96
+
97
+ const block = normalized.slice(braceStart + 1, cursor - 1);
98
+ const selectors = selectorPart.split(',').map((part) => part.trim());
99
+ if (selectors.includes(selector)) {
100
+ blocks.push(block);
101
+ }
102
+
103
+ index = cursor;
104
+ }
105
+
106
+ return blocks;
107
+ }
108
+
109
+ export function extractCssRuleBlock(css: string, selector: string): string | null {
110
+ const blocks = extractCssRuleBlocks(css, selector);
111
+ return blocks.length ? blocks.join('\n') : null;
112
+ }
113
+
114
+ export function cssBlockIncludes(block: string, declaration: string): boolean {
115
+ return block.replace(/\s+/g, ' ').includes(declaration.replace(/\s+/g, ' '));
116
+ }
117
+
118
+ export function validatePopupStyleContract(css: string, contract: Record<string, GcsaPopupStyleRule>) {
119
+ const failures: string[] = [];
120
+ for (const [name, rule] of Object.entries(contract)) {
121
+ const block = extractCssRuleBlock(css, rule.selector);
122
+ if (!block) {
123
+ failures.push(`${name}: missing selector ${rule.selector}`);
124
+ continue;
125
+ }
126
+ if (rule.padding && !cssBlockIncludes(block, `padding: ${rule.padding}`)) {
127
+ failures.push(`${name}: expected padding ${rule.padding}`);
128
+ }
129
+ if (rule.minWidth && !cssBlockIncludes(block, `min-width: ${rule.minWidth}`)) {
130
+ failures.push(`${name}: expected min-width ${rule.minWidth}`);
131
+ }
132
+ if (rule.maxWidth && !cssBlockIncludes(block, `max-width: ${rule.maxWidth}`)) {
133
+ failures.push(`${name}: expected max-width ${rule.maxWidth}`);
134
+ }
135
+ }
136
+ return failures;
137
+ }
@@ -0,0 +1,29 @@
1
+ import type { GcsaButtonSize } from './index';
2
+
3
+ /** 表单/控件密度,对齐 Ant Design:small / middle / large */
4
+ export type GcsaComponentSize = 'sm' | 'md' | 'lg';
5
+
6
+ export const GCSA_DEFAULT_COMPONENT_SIZE: GcsaComponentSize = 'md';
7
+
8
+ export function resolveGcsaComponentSize(
9
+ propSize: GcsaComponentSize | undefined,
10
+ configSize: GcsaComponentSize = GCSA_DEFAULT_COMPONENT_SIZE
11
+ ): GcsaComponentSize {
12
+ return propSize ?? configSize;
13
+ }
14
+
15
+ export function resolveGcsaButtonSize(
16
+ propSize: GcsaButtonSize | undefined,
17
+ configSize: GcsaComponentSize = GCSA_DEFAULT_COMPONENT_SIZE
18
+ ): GcsaButtonSize {
19
+ if (propSize) return propSize;
20
+ return configSize;
21
+ }
22
+
23
+ /** e.g. gcsaControlSizeClass('gcsa-text-control', 'md') → 'gcsa-text-control--md' */
24
+ export function gcsaControlSizeClass(
25
+ prefix: string,
26
+ size: GcsaComponentSize = GCSA_DEFAULT_COMPONENT_SIZE
27
+ ): string {
28
+ return `${prefix}--${size}`;
29
+ }
@@ -0,0 +1,36 @@
1
+ export type GcsaTableSortingEntry = { id: string; desc: boolean };
2
+
3
+ export type GcsaTableSortOrder = 'ascending' | 'descending' | null;
4
+
5
+ export type GcsaTableSortChange = {
6
+ prop: string | null;
7
+ order: GcsaTableSortOrder;
8
+ };
9
+
10
+ /** Stable column metadata exposed by legacy-compatible row/cell events. */
11
+ export type GcsaTableColumnInfo = {
12
+ id: string;
13
+ prop: string;
14
+ label: string;
15
+ width?: string;
16
+ minWidth?: string;
17
+ fixed?: 'left' | 'right' | true;
18
+ };
19
+
20
+ export type GcsaTableHeaderEvent = {
21
+ column: GcsaTableColumnInfo;
22
+ event: MouseEvent;
23
+ };
24
+
25
+ /** Maps the first TanStack sort entry to the payload used by Element tables. */
26
+ export function getGcsaTableSortChange(sorting: GcsaTableSortingEntry[]): GcsaTableSortChange {
27
+ const primary = sorting[0];
28
+ if (!primary) return { prop: null, order: null };
29
+ return { prop: primary.id, order: primary.desc ? 'descending' : 'ascending' };
30
+ }
31
+
32
+ /** Maps Element-style imperative sorting arguments to TanStack state. */
33
+ export function getGcsaTableSorting(prop: string, order: GcsaTableSortOrder): GcsaTableSortingEntry[] {
34
+ if (!prop || !order) return [];
35
+ return [{ id: prop, desc: order === 'descending' }];
36
+ }
@@ -0,0 +1,69 @@
1
+ export const GCSA_TABLE_EXPAND_COLUMN_WIDTH = 44;
2
+ export const GCSA_TABLE_SELECTION_COLUMN_WIDTH = 48;
3
+ export const GCSA_TABLE_DEFAULT_COLUMN_MIN_WIDTH = 120;
4
+ export const GCSA_TABLE_MIN_WIDTH_FLOOR = 640;
5
+
6
+ export type GcsaTableLayoutColumn = {
7
+ id: string;
8
+ fixed?: 'left' | 'right' | true;
9
+ width?: string;
10
+ minWidth?: string;
11
+ };
12
+
13
+ export type GcsaTableFixedOffsetOptions = {
14
+ expandable?: boolean;
15
+ selectable?: boolean;
16
+ fixedSelection?: boolean;
17
+ expandColumnWidth?: number;
18
+ selectionColumnWidth?: number;
19
+ columnWidths?: Record<string, number>;
20
+ };
21
+
22
+ function controlWidth(value: number | undefined, fallback: number) {
23
+ return Number.isFinite(value) && value! > 0 ? value! : fallback;
24
+ }
25
+
26
+ function columnWidth(column: GcsaTableLayoutColumn, widths: Record<string, number>) {
27
+ const override = widths[column.id];
28
+ if (Number.isFinite(override) && override > 0) return override;
29
+ const parsed = Number.parseFloat(column.width ?? column.minWidth ?? '');
30
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : 0;
31
+ }
32
+
33
+ export function getGcsaTableMinWidth(
34
+ columns: GcsaTableLayoutColumn[],
35
+ options: GcsaTableFixedOffsetOptions & { defaultColumnMinWidth?: number } = {}
36
+ ): number {
37
+ const defaultColumnMinWidth = options.defaultColumnMinWidth ?? GCSA_TABLE_DEFAULT_COLUMN_MIN_WIDTH;
38
+ const dataWidth = columns.reduce(
39
+ (sum, column) => sum + (columnWidth(column, options.columnWidths ?? {}) || defaultColumnMinWidth),
40
+ 0
41
+ );
42
+ const leadingControls =
43
+ (options.expandable ? controlWidth(options.expandColumnWidth, GCSA_TABLE_EXPAND_COLUMN_WIDTH) : 0) +
44
+ (options.selectable ? controlWidth(options.selectionColumnWidth, GCSA_TABLE_SELECTION_COLUMN_WIDTH) : 0);
45
+
46
+ return Math.max(GCSA_TABLE_MIN_WIDTH_FLOOR, dataWidth + leadingControls);
47
+ }
48
+
49
+ export function getGcsaTableFixedOffset(
50
+ columns: GcsaTableLayoutColumn[],
51
+ id: string,
52
+ direction: 'left' | 'right',
53
+ options: GcsaTableFixedOffsetOptions = {}
54
+ ) {
55
+ const targetIndex = columns.findIndex(column => column.id === id);
56
+ if (targetIndex < 0) return 0;
57
+
58
+ const leadingControlWidth = direction === 'left' && options.selectable && options.fixedSelection
59
+ ? controlWidth(options.selectionColumnWidth, GCSA_TABLE_SELECTION_COLUMN_WIDTH) +
60
+ (options.expandable ? controlWidth(options.expandColumnWidth, GCSA_TABLE_EXPAND_COLUMN_WIDTH) : 0)
61
+ : 0;
62
+ const candidates = direction === 'left'
63
+ ? columns.slice(0, targetIndex)
64
+ : columns.slice(targetIndex + 1);
65
+
66
+ return candidates
67
+ .filter(column => (column.fixed === true ? 'left' : column.fixed) === direction)
68
+ .reduce((offset, column) => offset + columnWidth(column, options.columnWidths ?? {}), leadingControlWidth);
69
+ }
@@ -0,0 +1,104 @@
1
+ import type { GcsaTableColumn, GcsaTableColumnFixed } from './index';
2
+
3
+ export type GcsaLegacyTableColumn<T> = Omit<Partial<GcsaTableColumn<T>>, 'sortable' | 'width' | 'minWidth' | 'id' | 'formatter' | 'columns'> & {
4
+ id?: string;
5
+ prop?: keyof T & string;
6
+ label?: string;
7
+ type?: 'selection' | 'index' | 'expand';
8
+ index?: number | ((index: number) => number | string);
9
+ selectable?: (row: T, index: number) => boolean;
10
+ sortable?: boolean | 'custom';
11
+ width?: string | number;
12
+ minWidth?: string | number;
13
+ 'min-width'?: string | number;
14
+ showOverflowTooltip?: boolean;
15
+ 'show-overflow-tooltip'?: boolean;
16
+ sortMethod?: (a: T, b: T) => number;
17
+ 'sort-method'?: (a: T, b: T) => number;
18
+ formatter?: (row: T, column: GcsaLegacyTableColumn<T>, value: unknown, index: number) => unknown;
19
+ children?: GcsaLegacyTableColumn<T>[];
20
+ fixed?: GcsaTableColumnFixed;
21
+ };
22
+
23
+ export type GcsaTableMigration<T> = {
24
+ columns: GcsaTableColumn<T>[];
25
+ selectable: boolean | ((row: T, index: number) => boolean);
26
+ expandable: boolean;
27
+ fixedSelection: boolean;
28
+ selectionColumnWidth?: number;
29
+ expandColumnWidth?: number;
30
+ };
31
+
32
+ export type GcsaTableMigrationOptions = {
33
+ page?: number;
34
+ pageSize?: number;
35
+ };
36
+
37
+ export function normalizeGcsaTableColumns<T extends Record<string, unknown>>(
38
+ columns: GcsaLegacyTableColumn<T>[],
39
+ options: GcsaTableMigrationOptions = {}
40
+ ): GcsaTableMigration<T> {
41
+ let selectable: GcsaTableMigration<T>['selectable'] = false;
42
+ let expandable = false;
43
+ let fixedSelection = false;
44
+ let selectionColumnWidth: number | undefined;
45
+ let expandColumnWidth: number | undefined;
46
+ const normalized: GcsaTableColumn<T>[] = [];
47
+ const page = Math.max(1, options.page ?? 1);
48
+ const pageSize = Math.max(1, options.pageSize ?? 10);
49
+ const indexOffset = (page - 1) * pageSize;
50
+
51
+ columns.forEach((column, index) => {
52
+ const { formatter: legacyFormatter, children: legacyChildren, ...columnRest } = column;
53
+ const type = column.type;
54
+ const width = normalizeColumnWidth(column.width);
55
+ const minWidth = normalizeColumnWidth(column.minWidth ?? column['min-width']);
56
+ const showOverflowTooltip = column.showOverflowTooltip ?? column['show-overflow-tooltip'];
57
+ const sortMethod = column.sortMethod ?? column['sort-method'];
58
+ if (type === 'selection') {
59
+ selectable = column.selectable ?? true;
60
+ fixedSelection ||= Boolean(column.fixed);
61
+ selectionColumnWidth ??= width ? Number.parseFloat(width) : undefined;
62
+ return;
63
+ }
64
+ if (type === 'expand') {
65
+ expandable = true;
66
+ expandColumnWidth ??= width ? Number.parseFloat(width) : undefined;
67
+ return;
68
+ }
69
+ const id = column.id ?? column.prop ?? (type === 'index' ? '__index' : `column-${index}`);
70
+ const header = column.header ?? column.label ?? (type === 'index' ? '#' : id);
71
+ const accessorKey = column.accessorKey ?? (column.prop as keyof T & string | undefined);
72
+ const children = legacyChildren?.length
73
+ ? normalizeGcsaTableColumns(legacyChildren, options).columns
74
+ : undefined;
75
+ normalized.push({
76
+ ...columnRest,
77
+ id,
78
+ header,
79
+ ...(accessorKey ? { accessorKey } : {}),
80
+ ...(children ? { columns: children } : {}),
81
+ sortable: column.sortable === 'custom' ? true : column.sortable,
82
+ ...(sortMethod ? { sortCompare: sortMethod } : {}),
83
+ ...(legacyFormatter ? { formatter: (value: unknown, row: T, rowIndex: number) => legacyFormatter(row, column, value, rowIndex) } : {}),
84
+ width,
85
+ minWidth,
86
+ showOverflowTooltip,
87
+ ...(type === 'index' ? {
88
+ formatter: (_value: unknown, _row: T, rowIndex: number) => {
89
+ const absoluteIndex = indexOffset + rowIndex;
90
+ if (typeof column.index === 'function') return column.index(absoluteIndex);
91
+ if (typeof column.index === 'number') return column.index + absoluteIndex;
92
+ return absoluteIndex + 1;
93
+ }
94
+ } : {})
95
+ });
96
+ });
97
+
98
+ return { columns: normalized, selectable, expandable, fixedSelection, selectionColumnWidth, expandColumnWidth };
99
+ }
100
+
101
+ function normalizeColumnWidth(value: string | number | undefined) {
102
+ if (value === undefined || value === null || value === '') return undefined;
103
+ return typeof value === 'number' ? `${value}px` : /^\d+(?:\.\d+)?$/.test(value) ? `${value}px` : value;
104
+ }
@@ -0,0 +1,61 @@
1
+ export type GcsaTableSortingPreference = { id: string; desc: boolean };
2
+
3
+ export type GcsaTablePreferences = {
4
+ columnVisibility?: Record<string, boolean>;
5
+ columnWidths?: Record<string, number>;
6
+ sorting?: GcsaTableSortingPreference[];
7
+ pageSize?: number;
8
+ };
9
+
10
+ export type GcsaTablePreferenceStorage = Pick<Storage, 'getItem' | 'setItem' | 'removeItem'>;
11
+
12
+ function isRecord(value: unknown): value is Record<string, unknown> {
13
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
14
+ }
15
+
16
+ function normalizeRecord<T>(value: unknown, predicate: (entry: unknown) => entry is T): Record<string, T> | undefined {
17
+ if (!isRecord(value)) return undefined;
18
+ const entries = Object.entries(value).filter(([, entry]) => predicate(entry));
19
+ return entries.length ? Object.fromEntries(entries) as Record<string, T> : undefined;
20
+ }
21
+
22
+ export function normalizeGcsaTablePreferences(value: unknown): GcsaTablePreferences {
23
+ if (!isRecord(value)) return {};
24
+ const columnVisibility = normalizeRecord(value.columnVisibility, (entry): entry is boolean => typeof entry === 'boolean');
25
+ const columnWidths = normalizeRecord(value.columnWidths, (entry): entry is number => typeof entry === 'number' && Number.isFinite(entry) && entry > 0);
26
+ const sorting = Array.isArray(value.sorting)
27
+ ? value.sorting.filter(isRecord).filter(entry => typeof entry.id === 'string' && typeof entry.desc === 'boolean').map(entry => ({ id: entry.id as string, desc: entry.desc as boolean }))
28
+ : undefined;
29
+ const pageSize = typeof value.pageSize === 'number' && Number.isInteger(value.pageSize) && value.pageSize > 0 ? value.pageSize : undefined;
30
+ return { columnVisibility, columnWidths, sorting: sorting?.length ? sorting : undefined, pageSize };
31
+ }
32
+
33
+ export function readGcsaTablePreferences(storage: GcsaTablePreferenceStorage | undefined, key: string): GcsaTablePreferences {
34
+ if (!storage || !key) return {};
35
+ try {
36
+ const raw = storage.getItem(key);
37
+ return raw ? normalizeGcsaTablePreferences(JSON.parse(raw)) : {};
38
+ } catch {
39
+ return {};
40
+ }
41
+ }
42
+
43
+ export function writeGcsaTablePreferences(storage: GcsaTablePreferenceStorage | undefined, key: string, value: GcsaTablePreferences): boolean {
44
+ if (!storage || !key) return false;
45
+ try {
46
+ storage.setItem(key, JSON.stringify(normalizeGcsaTablePreferences(value)));
47
+ return true;
48
+ } catch {
49
+ return false;
50
+ }
51
+ }
52
+
53
+ export function clearGcsaTablePreferences(storage: GcsaTablePreferenceStorage | undefined, key: string): boolean {
54
+ if (!storage || !key) return false;
55
+ try {
56
+ storage.removeItem(key);
57
+ return true;
58
+ } catch {
59
+ return false;
60
+ }
61
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Resolves the row id used by TanStack Table and the legacy table API.
3
+ * TanStack's default fallback is relative to the parent row; keeping that
4
+ * behavior here prevents parent and child rows from sharing an id in trees.
5
+ */
6
+ export function getGcsaTableRowId<T extends Record<string, unknown>>(
7
+ row: T,
8
+ index: number,
9
+ rowKey?: keyof T & string | ((row: T, index: number) => string),
10
+ parentId?: string
11
+ ) {
12
+ if (typeof rowKey === 'function') return rowKey(row, index);
13
+ if (rowKey) return String(row[rowKey]);
14
+ return parentId ? `${parentId}.${index}` : String(index);
15
+ }
16
+
17
+ export function getGcsaTableDefaultExpandedKeys<T extends Record<string, unknown>>(
18
+ rows: T[],
19
+ treeData: boolean,
20
+ childrenKey: keyof T & string,
21
+ rowKey?: keyof T & string | ((row: T, index: number) => string)
22
+ ) {
23
+ if (!treeData) return rows.map((row, index) => getGcsaTableRowId(row, index, rowKey));
24
+ const keys: string[] = [];
25
+ const visit = (items: T[], parentPath = '') => {
26
+ items.forEach((row, index) => {
27
+ const children = (row as Record<string, unknown>)[childrenKey];
28
+ if (!Array.isArray(children) || !children.length) return;
29
+ const path = parentPath ? `${parentPath}.${index}` : String(index);
30
+ keys.push(rowKey ? getGcsaTableRowId(row, index, rowKey) : path);
31
+ visit(children as T[], path);
32
+ });
33
+ };
34
+ visit(rows);
35
+ return keys;
36
+ }
@@ -0,0 +1,83 @@
1
+ /** 24 小时制时间字符串 HH:mm */
2
+ export type GcsaTimeString = string;
3
+
4
+ export type GcsaTimePickerOptions = {
5
+ min?: GcsaTimeString;
6
+ max?: GcsaTimeString;
7
+ /** 步长,单位秒,默认 60 */
8
+ step?: number;
9
+ };
10
+
11
+ export const GCSA_TIME_STRING_PATTERN = /^\d{2}:\d{2}$/;
12
+ export const GCSA_TIME_PICKER_CELL_HEIGHT = 32;
13
+
14
+ export function isGcsaTimeString(value: string): value is GcsaTimeString {
15
+ return GCSA_TIME_STRING_PATTERN.test(value);
16
+ }
17
+
18
+ export function gcsaTimeToMinutes(value?: string): number | undefined {
19
+ if (!value || !GCSA_TIME_STRING_PATTERN.test(value)) return undefined;
20
+ const [hours, minutes] = value.split(':').map(Number);
21
+ return hours * 60 + minutes;
22
+ }
23
+
24
+ export function gcsaMinutesToTime(totalMinutes: number): GcsaTimeString {
25
+ const normalized = Math.max(0, Math.min(1439, totalMinutes));
26
+ const hours = Math.floor(normalized / 60);
27
+ const minutes = normalized % 60;
28
+ return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}`;
29
+ }
30
+
31
+ export function gcsaNormalizeTimeStep(step?: number): number {
32
+ const seconds = Number(step);
33
+ if (!Number.isFinite(seconds) || seconds <= 0) return 60;
34
+ return Math.max(60, Math.round(seconds / 60) * 60);
35
+ }
36
+
37
+ export function gcsaBuildTimeOptions({ min, max, step }: GcsaTimePickerOptions = {}): GcsaTimeString[] {
38
+ const incrementMinutes = gcsaNormalizeTimeStep(step) / 60;
39
+ const start = Math.max(0, gcsaTimeToMinutes(min) ?? 0);
40
+ const end = Math.min(1439, gcsaTimeToMinutes(max) ?? 1439);
41
+ const result: GcsaTimeString[] = [];
42
+
43
+ for (let value = start; value <= end; value += incrementMinutes) {
44
+ result.push(gcsaMinutesToTime(value));
45
+ }
46
+
47
+ return result.length ? result : [gcsaMinutesToTime(start)];
48
+ }
49
+
50
+ export function gcsaTimePickerHours(options: GcsaTimeString[]): string[] {
51
+ return [...new Set(options.map(option => option.slice(0, 2)))];
52
+ }
53
+
54
+ export function gcsaTimePickerMinutes(options: GcsaTimeString[], hour: string): string[] {
55
+ return options.filter(option => option.startsWith(`${hour}:`)).map(option => option.slice(3));
56
+ }
57
+
58
+ export function gcsaResolveTimeDraft(value: string | undefined, options: GcsaTimeString[]): GcsaTimeString {
59
+ if (value && options.includes(value)) return value;
60
+ return options[0] ?? '00:00';
61
+ }
62
+
63
+ export function gcsaNearestTimeOption(options: GcsaTimeString[], targetMinutes: number): GcsaTimeString {
64
+ return options.reduce((best, option) => {
65
+ const optionMinutes = gcsaTimeToMinutes(option) ?? 0;
66
+ const bestMinutes = gcsaTimeToMinutes(best) ?? 0;
67
+ return Math.abs(optionMinutes - targetMinutes) < Math.abs(bestMinutes - targetMinutes) ? option : best;
68
+ }, options[0] ?? '00:00');
69
+ }
70
+
71
+ export function gcsaCurrentTimeOption(options: GcsaTimeString[]): GcsaTimeString {
72
+ const now = new Date();
73
+ return gcsaNearestTimeOption(options, now.getHours() * 60 + now.getMinutes());
74
+ }
75
+
76
+ export function gcsaTimePickerScrollTop(index: number): number {
77
+ return Math.max(0, index) * GCSA_TIME_PICKER_CELL_HEIGHT;
78
+ }
79
+
80
+ export function gcsaTimePickerScrollIndex(scrollTop: number, itemCount: number): number {
81
+ if (!itemCount) return 0;
82
+ return Math.max(0, Math.min(itemCount - 1, Math.round(scrollTop / GCSA_TIME_PICKER_CELL_HEIGHT)));
83
+ }