@sincpro/web-ui 0.0.0-stage → 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.
- package/LICENSE +21 -0
- package/README.md +157 -2
- package/dist/components/assist/AssistMark.d.ts +64 -0
- package/dist/components/assist/AssistMark.js +108 -0
- package/dist/components/assist/AssistPending.d.ts +24 -0
- package/dist/components/assist/AssistPending.js +26 -0
- package/dist/components/assist/AssistSuggestion.d.ts +42 -0
- package/dist/components/assist/AssistSuggestion.js +55 -0
- package/dist/components/assist/ChangeReview.d.ts +56 -0
- package/dist/components/assist/ChangeReview.js +121 -0
- package/dist/components/assist/assist.d.ts +77 -0
- package/dist/components/assist/assist.js +87 -0
- package/dist/components/assist/index.d.ts +5 -0
- package/dist/components/assist/index.js +7 -0
- package/dist/components/chatter/Chatter.d.ts +161 -0
- package/dist/components/chatter/Chatter.js +232 -0
- package/dist/components/chatter/Messages.d.ts +66 -0
- package/dist/components/chatter/Messages.js +43 -0
- package/dist/components/chatter/Timeline.d.ts +21 -0
- package/dist/components/chatter/Timeline.js +30 -0
- package/dist/components/chatter/index.d.ts +3 -0
- package/dist/components/chatter/index.js +4 -0
- package/dist/components/form/Form.d.ts +52 -0
- package/dist/components/form/Form.js +49 -0
- package/dist/components/form/Sheet.d.ts +43 -0
- package/dist/components/form/Sheet.js +83 -0
- package/dist/components/form/index.d.ts +2 -0
- package/dist/components/form/index.js +4 -0
- package/dist/components/frame/Chart.d.ts +13 -0
- package/dist/components/frame/Chart.js +24 -0
- package/dist/components/frame/FrameView.d.ts +33 -0
- package/dist/components/frame/FrameView.js +125 -0
- package/dist/components/frame/index.d.ts +3 -0
- package/dist/components/frame/index.js +3 -0
- package/dist/components/frame/model.d.ts +43 -0
- package/dist/components/frame/model.js +101 -0
- package/dist/components/index.d.ts +10 -0
- package/dist/components/index.js +11 -0
- package/dist/components/lines/LineItems.d.ts +74 -0
- package/dist/components/lines/LineItems.js +229 -0
- package/dist/components/lines/index.d.ts +1 -0
- package/dist/components/lines/index.js +2 -0
- package/dist/components/lines/lines.d.ts +23 -0
- package/dist/components/lines/lines.js +26 -0
- package/dist/components/listing/ColumnsMenu.d.ts +20 -0
- package/dist/components/listing/ColumnsMenu.js +46 -0
- package/dist/components/listing/GroupingLevels.d.ts +35 -0
- package/dist/components/listing/GroupingLevels.js +45 -0
- package/dist/components/listing/Listing.d.ts +27 -0
- package/dist/components/listing/Listing.js +86 -0
- package/dist/components/listing/ListingBar.d.ts +73 -0
- package/dist/components/listing/ListingBar.js +105 -0
- package/dist/components/listing/Pagination.d.ts +36 -0
- package/dist/components/listing/Pagination.js +69 -0
- package/dist/components/listing/RecordCard.d.ts +33 -0
- package/dist/components/listing/RecordCard.js +22 -0
- package/dist/components/listing/RecordLink.d.ts +7 -0
- package/dist/components/listing/RecordLink.js +22 -0
- package/dist/components/listing/Row.d.ts +106 -0
- package/dist/components/listing/Row.js +122 -0
- package/dist/components/listing/SortMenu.d.ts +62 -0
- package/dist/components/listing/SortMenu.js +105 -0
- package/dist/components/listing/ViewSwitcher.d.ts +24 -0
- package/dist/components/listing/ViewSwitcher.js +16 -0
- package/dist/components/listing/columns.d.ts +47 -0
- package/dist/components/listing/columns.js +72 -0
- package/dist/components/listing/grouping.d.ts +35 -0
- package/dist/components/listing/grouping.js +47 -0
- package/dist/components/listing/index.d.ts +17 -0
- package/dist/components/listing/index.js +20 -0
- package/dist/components/listing/modelColumns.d.ts +50 -0
- package/dist/components/listing/modelColumns.js +103 -0
- package/dist/components/listing/pages.d.ts +78 -0
- package/dist/components/listing/pages.js +135 -0
- package/dist/components/listing/selection.d.ts +21 -0
- package/dist/components/listing/selection.js +43 -0
- package/dist/components/listing/set.d.ts +48 -0
- package/dist/components/listing/set.js +24 -0
- package/dist/components/listing/tree.d.ts +28 -0
- package/dist/components/listing/tree.js +23 -0
- package/dist/components/navigation/CommandPalette.d.ts +40 -0
- package/dist/components/navigation/CommandPalette.js +114 -0
- package/dist/components/navigation/NavigationTree.d.ts +31 -0
- package/dist/components/navigation/NavigationTree.js +79 -0
- package/dist/components/navigation/commands.d.ts +30 -0
- package/dist/components/navigation/commands.js +30 -0
- package/dist/components/navigation/index.d.ts +3 -0
- package/dist/components/navigation/index.js +4 -0
- package/dist/components/record/Breadcrumb.d.ts +42 -0
- package/dist/components/record/Breadcrumb.js +98 -0
- package/dist/components/record/DirtyActions.d.ts +26 -0
- package/dist/components/record/DirtyActions.js +22 -0
- package/dist/components/record/Pager.d.ts +18 -0
- package/dist/components/record/Pager.js +33 -0
- package/dist/components/record/RecordHeader.d.ts +71 -0
- package/dist/components/record/RecordHeader.js +72 -0
- package/dist/components/record/RecordSkeleton.d.ts +3 -0
- package/dist/components/record/RecordSkeleton.js +21 -0
- package/dist/components/record/SmartButtons.d.ts +35 -0
- package/dist/components/record/SmartButtons.js +38 -0
- package/dist/components/record/StatusBar.d.ts +51 -0
- package/dist/components/record/StatusBar.js +168 -0
- package/dist/components/record/ThumbBar.d.ts +36 -0
- package/dist/components/record/ThumbBar.js +86 -0
- package/dist/components/record/index.d.ts +6 -0
- package/dist/components/record/index.js +8 -0
- package/dist/components/search/CriteriaBuilder.d.ts +58 -0
- package/dist/components/search/CriteriaBuilder.js +278 -0
- package/dist/components/search/CriteriaChips.d.ts +22 -0
- package/dist/components/search/CriteriaChips.js +64 -0
- package/dist/components/search/FilterBuilder.d.ts +142 -0
- package/dist/components/search/FilterBuilder.js +421 -0
- package/dist/components/search/JoinWord.d.ts +8 -0
- package/dist/components/search/JoinWord.js +47 -0
- package/dist/components/search/JsonEditor.d.ts +19 -0
- package/dist/components/search/JsonEditor.js +78 -0
- package/dist/components/search/SearchBar.d.ts +78 -0
- package/dist/components/search/SearchBar.js +387 -0
- package/dist/components/search/criteria.d.ts +116 -0
- package/dist/components/search/criteria.js +159 -0
- package/dist/components/search/datePeriods.d.ts +122 -0
- package/dist/components/search/datePeriods.js +134 -0
- package/dist/components/search/dateRanges.d.ts +54 -0
- package/dist/components/search/dateRanges.js +89 -0
- package/dist/components/search/index.d.ts +9 -0
- package/dist/components/search/index.js +20 -0
- package/dist/components/search/model.d.ts +54 -0
- package/dist/components/search/model.js +37 -0
- package/dist/components/search/query.d.ts +166 -0
- package/dist/components/search/query.js +436 -0
- package/dist/components/shell/AppFrame.d.ts +14 -0
- package/dist/components/shell/AppFrame.js +80 -0
- package/dist/components/shell/TopBar.d.ts +73 -0
- package/dist/components/shell/TopBar.js +54 -0
- package/dist/components/shell/headerSlot.d.ts +17 -0
- package/dist/components/shell/headerSlot.js +21 -0
- package/dist/components/shell/index.d.ts +2 -0
- package/dist/components/shell/index.js +3 -0
- package/dist/foundations/aria.d.ts +14 -0
- package/dist/foundations/aria.js +18 -0
- package/dist/foundations/calendar.d.ts +28 -0
- package/dist/foundations/calendar.js +183 -0
- package/dist/foundations/copy.d.ts +619 -0
- package/dist/foundations/copy.es.d.ts +2 -0
- package/dist/foundations/copy.es.js +377 -0
- package/dist/foundations/copy.js +472 -0
- package/dist/foundations/density.d.ts +16 -0
- package/dist/foundations/density.js +21 -0
- package/dist/foundations/grouping.d.ts +281 -0
- package/dist/foundations/grouping.js +0 -0
- package/dist/foundations/index.d.ts +14 -0
- package/dist/foundations/index.js +30 -0
- package/dist/foundations/layout.d.ts +9 -0
- package/dist/foundations/layout.js +9 -0
- package/dist/foundations/pivot.d.ts +222 -0
- package/dist/foundations/pivot.js +198 -0
- package/dist/foundations/pivotSheet.d.ts +61 -0
- package/dist/foundations/pivotSheet.js +0 -0
- package/dist/foundations/slots.d.ts +67 -0
- package/dist/foundations/slots.js +63 -0
- package/dist/foundations/sticky.d.ts +53 -0
- package/dist/foundations/sticky.js +150 -0
- package/dist/foundations/surface.d.ts +9 -0
- package/dist/foundations/surface.js +26 -0
- package/dist/foundations/theme.d.ts +42 -0
- package/dist/foundations/theme.js +88 -0
- package/dist/foundations/types.d.ts +23 -0
- package/dist/foundations/types.js +50 -0
- package/dist/foundations/variants.d.ts +19 -0
- package/dist/foundations/variants.js +48 -0
- package/dist/foundations/xlsx.d.ts +86 -0
- package/dist/foundations/xlsx.js +0 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +19 -0
- package/dist/primitives/block/Block.d.ts +66 -0
- package/dist/primitives/block/Block.js +34 -0
- package/dist/primitives/block/Line.d.ts +18 -0
- package/dist/primitives/block/Line.js +45 -0
- package/dist/primitives/block/SectionGrid.d.ts +30 -0
- package/dist/primitives/block/SectionGrid.js +29 -0
- package/dist/primitives/block/Steps.d.ts +76 -0
- package/dist/primitives/block/Steps.js +43 -0
- package/dist/primitives/block/index.d.ts +4 -0
- package/dist/primitives/block/index.js +7 -0
- package/dist/primitives/button/Button.d.ts +137 -0
- package/dist/primitives/button/Button.js +234 -0
- package/dist/primitives/button/index.d.ts +1 -0
- package/dist/primitives/button/index.js +2 -0
- package/dist/primitives/cell/CellStack.d.ts +47 -0
- package/dist/primitives/cell/CellStack.js +33 -0
- package/dist/primitives/cell/index.d.ts +1 -0
- package/dist/primitives/cell/index.js +3 -0
- package/dist/primitives/choice/ComboBox.d.ts +16 -0
- package/dist/primitives/choice/ComboBox.js +52 -0
- package/dist/primitives/choice/RadioGroup.d.ts +18 -0
- package/dist/primitives/choice/RadioGroup.js +50 -0
- package/dist/primitives/choice/SearchField.d.ts +24 -0
- package/dist/primitives/choice/SearchField.js +27 -0
- package/dist/primitives/choice/SegmentedControl.d.ts +16 -0
- package/dist/primitives/choice/SegmentedControl.js +69 -0
- package/dist/primitives/choice/Slider.d.ts +16 -0
- package/dist/primitives/choice/Slider.js +46 -0
- package/dist/primitives/choice/Switch.d.ts +14 -0
- package/dist/primitives/choice/Switch.js +65 -0
- package/dist/primitives/choice/ThemeSwitcher.d.ts +10 -0
- package/dist/primitives/choice/ThemeSwitcher.js +25 -0
- package/dist/primitives/choice/index.d.ts +8 -0
- package/dist/primitives/choice/index.js +11 -0
- package/dist/primitives/choice/options.d.ts +28 -0
- package/dist/primitives/choice/options.js +6 -0
- package/dist/primitives/date/Calendar.d.ts +31 -0
- package/dist/primitives/date/Calendar.js +110 -0
- package/dist/primitives/date/DatePicker.d.ts +34 -0
- package/dist/primitives/date/DatePicker.js +99 -0
- package/dist/primitives/date/index.d.ts +2 -0
- package/dist/primitives/date/index.js +4 -0
- package/dist/primitives/dialog/Dialog.d.ts +118 -0
- package/dist/primitives/dialog/Dialog.js +121 -0
- package/dist/primitives/dialog/index.d.ts +1 -0
- package/dist/primitives/dialog/index.js +2 -0
- package/dist/primitives/disclosure/Disclosure.d.ts +36 -0
- package/dist/primitives/disclosure/Disclosure.js +52 -0
- package/dist/primitives/disclosure/index.d.ts +1 -0
- package/dist/primitives/disclosure/index.js +2 -0
- package/dist/primitives/field/Checks.d.ts +57 -0
- package/dist/primitives/field/Checks.js +72 -0
- package/dist/primitives/field/DropZone.d.ts +42 -0
- package/dist/primitives/field/DropZone.js +52 -0
- package/dist/primitives/field/Field.d.ts +166 -0
- package/dist/primitives/field/Field.js +240 -0
- package/dist/primitives/field/Suggest.d.ts +48 -0
- package/dist/primitives/field/Suggest.js +43 -0
- package/dist/primitives/field/aria.d.ts +61 -0
- package/dist/primitives/field/aria.js +70 -0
- package/dist/primitives/field/context.d.ts +30 -0
- package/dist/primitives/field/context.js +13 -0
- package/dist/primitives/field/index.d.ts +7 -0
- package/dist/primitives/field/index.js +11 -0
- package/dist/primitives/field/layout.d.ts +24 -0
- package/dist/primitives/field/layout.js +10 -0
- package/dist/primitives/field/mode.d.ts +9 -0
- package/dist/primitives/field/mode.js +13 -0
- package/dist/primitives/hint/Hint.d.ts +31 -0
- package/dist/primitives/hint/Hint.js +42 -0
- package/dist/primitives/hint/index.d.ts +1 -0
- package/dist/primitives/hint/index.js +1 -0
- package/dist/primitives/index.d.ts +19 -0
- package/dist/primitives/index.js +20 -0
- package/dist/primitives/menu/Menu.d.ts +100 -0
- package/dist/primitives/menu/Menu.js +59 -0
- package/dist/primitives/menu/index.d.ts +1 -0
- package/dist/primitives/menu/index.js +2 -0
- package/dist/primitives/notice/Callout.d.ts +49 -0
- package/dist/primitives/notice/Callout.js +28 -0
- package/dist/primitives/notice/Notice.d.ts +45 -0
- package/dist/primitives/notice/Notice.js +61 -0
- package/dist/primitives/notice/Toast.d.ts +55 -0
- package/dist/primitives/notice/Toast.js +142 -0
- package/dist/primitives/notice/index.d.ts +3 -0
- package/dist/primitives/notice/index.js +5 -0
- package/dist/primitives/overlay/Popover.d.ts +6 -0
- package/dist/primitives/overlay/Popover.js +27 -0
- package/dist/primitives/overlay/PopoverTrigger.d.ts +55 -0
- package/dist/primitives/overlay/PopoverTrigger.js +38 -0
- package/dist/primitives/overlay/Tooltip.d.ts +38 -0
- package/dist/primitives/overlay/Tooltip.js +74 -0
- package/dist/primitives/overlay/index.d.ts +3 -0
- package/dist/primitives/overlay/index.js +5 -0
- package/dist/primitives/picker/Picker.d.ts +48 -0
- package/dist/primitives/picker/Picker.js +150 -0
- package/dist/primitives/picker/index.d.ts +1 -0
- package/dist/primitives/picker/index.js +2 -0
- package/dist/primitives/surface/Peek.d.ts +60 -0
- package/dist/primitives/surface/Peek.js +71 -0
- package/dist/primitives/surface/index.d.ts +1 -0
- package/dist/primitives/surface/index.js +2 -0
- package/dist/primitives/table/CellEditor.d.ts +14 -0
- package/dist/primitives/table/CellEditor.js +83 -0
- package/dist/primitives/table/DataTable.d.ts +135 -0
- package/dist/primitives/table/DataTable.js +244 -0
- package/dist/primitives/table/EditableTable.d.ts +96 -0
- package/dist/primitives/table/EditableTable.js +423 -0
- package/dist/primitives/table/PivotTable.d.ts +14 -0
- package/dist/primitives/table/PivotTable.js +0 -0
- package/dist/primitives/table/ReportTable.d.ts +38 -0
- package/dist/primitives/table/ReportTable.js +106 -0
- package/dist/primitives/table/RowActions.d.ts +16 -0
- package/dist/primitives/table/RowActions.js +28 -0
- package/dist/primitives/table/SheetTable.d.ts +70 -0
- package/dist/primitives/table/SheetTable.js +197 -0
- package/dist/primitives/table/Table.d.ts +106 -0
- package/dist/primitives/table/Table.js +116 -0
- package/dist/primitives/table/index.d.ts +7 -0
- package/dist/primitives/table/index.js +9 -0
- package/dist/primitives/tabs/Tabs.d.ts +91 -0
- package/dist/primitives/tabs/Tabs.js +56 -0
- package/dist/primitives/tabs/index.d.ts +1 -0
- package/dist/primitives/tabs/index.js +1 -0
- package/dist/primitives/tag/Tag.d.ts +36 -0
- package/dist/primitives/tag/Tag.js +68 -0
- package/dist/primitives/tag/Tags.d.ts +57 -0
- package/dist/primitives/tag/Tags.js +104 -0
- package/dist/primitives/tag/TagsField.d.ts +32 -0
- package/dist/primitives/tag/TagsField.js +182 -0
- package/dist/primitives/tag/choices.d.ts +38 -0
- package/dist/primitives/tag/choices.js +26 -0
- package/dist/primitives/tag/index.d.ts +4 -0
- package/dist/primitives/tag/index.js +8 -0
- package/dist/primitives/toolbar/Toolbar.d.ts +13 -0
- package/dist/primitives/toolbar/Toolbar.js +36 -0
- package/dist/primitives/toolbar/index.d.ts +1 -0
- package/dist/primitives/toolbar/index.js +2 -0
- package/dist/primitives/value/Copyable.d.ts +37 -0
- package/dist/primitives/value/Copyable.js +50 -0
- package/dist/primitives/value/Fecha.d.ts +12 -0
- package/dist/primitives/value/Fecha.js +80 -0
- package/dist/primitives/value/Meter.d.ts +29 -0
- package/dist/primitives/value/Meter.js +46 -0
- package/dist/primitives/value/Metric.d.ts +42 -0
- package/dist/primitives/value/Metric.js +38 -0
- package/dist/primitives/value/Person.d.ts +90 -0
- package/dist/primitives/value/Person.js +208 -0
- package/dist/primitives/value/Relation.d.ts +69 -0
- package/dist/primitives/value/Relation.js +57 -0
- package/dist/primitives/value/Totals.d.ts +26 -0
- package/dist/primitives/value/Totals.js +4 -0
- package/dist/primitives/value/Value.d.ts +77 -0
- package/dist/primitives/value/Value.js +119 -0
- package/dist/primitives/value/index.d.ts +8 -0
- package/dist/primitives/value/index.js +9 -0
- package/dist/tokens.css +1025 -0
- package/dist/views/index.d.ts +2 -0
- package/dist/views/index.js +3 -0
- package/dist/views/list/List.d.ts +113 -0
- package/dist/views/list/List.js +97 -0
- package/dist/views/list/ListView.d.ts +6 -0
- package/dist/views/list/ListView.js +269 -0
- package/dist/views/list/columns.d.ts +18 -0
- package/dist/views/list/columns.js +27 -0
- package/dist/views/list/derive.d.ts +76 -0
- package/dist/views/list/derive.js +181 -0
- package/dist/views/list/index.d.ts +2 -0
- package/dist/views/list/index.js +4 -0
- package/dist/views/list/presentations.d.ts +39 -0
- package/dist/views/list/presentations.js +103 -0
- package/dist/views/list/types.d.ts +272 -0
- package/dist/views/list/types.js +1 -0
- package/dist/views/list/useNarrowList.d.ts +17 -0
- package/dist/views/list/useNarrowList.js +32 -0
- package/dist/views/record/Record.d.ts +126 -0
- package/dist/views/record/Record.js +141 -0
- package/dist/views/record/index.d.ts +1 -0
- package/dist/views/record/index.js +2 -0
- package/package.json +109 -4
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { pathKey, pathLabels, pileLabel, } from "./grouping.js";
|
|
2
|
+
/**
|
|
3
|
+
* CÓMO SE LEE UNA CLAVE del eje, tramo por tramo.
|
|
4
|
+
*
|
|
5
|
+
* La clave identifica —«2026-09» es lo que el motor escribe y lo que se compara
|
|
6
|
+
* con lo que contó el servidor— y el rótulo se lee. Son dos textos, igual que en
|
|
7
|
+
* un montón, y por la misma razón: con el rótulo por identidad, dos septiembres
|
|
8
|
+
* de años distintos serían el mismo.
|
|
9
|
+
*
|
|
10
|
+
* `within` sale de los niveles de ARRIBA del mismo campo: con el año afuera, el
|
|
11
|
+
* mes se dice «Septiembre» y no «Septiembre de 2026», porque eso ya está escrito
|
|
12
|
+
* dos columnas antes.
|
|
13
|
+
*/
|
|
14
|
+
export function axisLabels(axis, key, label = pileLabel) {
|
|
15
|
+
return pathLabels(axis.levels, key, label);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* El separador entre las dos rutas de un cruce.
|
|
19
|
+
*
|
|
20
|
+
* Es un carácter distinto del que separa los tramos de una ruta —ése es el NUL
|
|
21
|
+
* de `pathKey`— porque si fuera el mismo, una fila `["a"]` cruzada con una
|
|
22
|
+
* columna `["b","c"]` y una fila `["a","b"]` cruzada con `["c"]` producirían la
|
|
23
|
+
* misma clave, y serían dos celdas distintas de la grilla apuntando al mismo
|
|
24
|
+
* número.
|
|
25
|
+
*/
|
|
26
|
+
const CROSS = "";
|
|
27
|
+
function crossKey(row, column) {
|
|
28
|
+
return `${pathKey(row)}${CROSS}${pathKey(column)}`;
|
|
29
|
+
}
|
|
30
|
+
/** Sólo lo que es una celda, sin las claves con las que viajó. */
|
|
31
|
+
function cellOf(one) {
|
|
32
|
+
return { count: one.count, values: one.values };
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Indexa un pivote que ya viene plegado.
|
|
36
|
+
*
|
|
37
|
+
* Los cruces viajan como lista —es lo que sabe serializar cualquier transporte—
|
|
38
|
+
* y se dibujan preguntando de a uno, así que en el medio hace falta un índice.
|
|
39
|
+
* Se arma una vez acá y no en cada celda de cada render: una grilla de 288
|
|
40
|
+
* celdas buscando en una lista de 40 son 11.520 comparaciones por pintada.
|
|
41
|
+
*/
|
|
42
|
+
export function pivotOf(source) {
|
|
43
|
+
const crossings = new Map();
|
|
44
|
+
for (const one of source.crossings)
|
|
45
|
+
crossings.set(crossKey(one.row, one.column), cellOf(one));
|
|
46
|
+
const rowMargins = new Map();
|
|
47
|
+
for (const one of source.rowMargins ?? [])
|
|
48
|
+
rowMargins.set(pathKey(one.key), cellOf(one));
|
|
49
|
+
const columnMargins = new Map();
|
|
50
|
+
for (const one of source.columnMargins ?? [])
|
|
51
|
+
columnMargins.set(pathKey(one.key), cellOf(one));
|
|
52
|
+
return {
|
|
53
|
+
rows: source.rows,
|
|
54
|
+
columns: source.columns,
|
|
55
|
+
measures: source.measures,
|
|
56
|
+
cell: (row, column) => crossings.get(crossKey(row, column)),
|
|
57
|
+
/* Los márgenes se ofrecen sólo si vinieron. La alternativa —contestar
|
|
58
|
+
siempre y devolver `undefined` adentro— haría que la grilla dibujara una
|
|
59
|
+
banda de totales entera llena de agujeros. */
|
|
60
|
+
...(source.rowMargins === undefined
|
|
61
|
+
? {}
|
|
62
|
+
: { rowTotal: (row) => rowMargins.get(pathKey(row)) }),
|
|
63
|
+
...(source.columnMargins === undefined
|
|
64
|
+
? {}
|
|
65
|
+
: { columnTotal: (column) => columnMargins.get(pathKey(column)) }),
|
|
66
|
+
...(source.total === undefined ? {} : { total: source.total }),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/** Las filas de un eje, agrupadas por su clave, en orden de primera aparición.
|
|
70
|
+
* El orden de las filas ya lo decidió alguien —una columna, un criterio, el
|
|
71
|
+
* motor— y reordenar los ejes acá lo contradiría. */
|
|
72
|
+
function axisBuckets(items, levels, of) {
|
|
73
|
+
const out = new Map();
|
|
74
|
+
for (const item of items) {
|
|
75
|
+
const key = levels.map((level) => of(item, level));
|
|
76
|
+
const id = pathKey(key);
|
|
77
|
+
const bucket = out.get(id);
|
|
78
|
+
if (bucket === undefined)
|
|
79
|
+
out.set(id, { key, items: [item] });
|
|
80
|
+
else
|
|
81
|
+
bucket.items.push(item);
|
|
82
|
+
}
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
85
|
+
function fold(measures, items) {
|
|
86
|
+
const values = {};
|
|
87
|
+
for (const measure of measures)
|
|
88
|
+
values[measure.id] = measure.fold(items);
|
|
89
|
+
return { count: items.length, values };
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Arma el pivote con las filas que hay EN LA MANO.
|
|
93
|
+
*
|
|
94
|
+
* Sirve cuando el conjunto entero está en el navegador. Cuando no lo está, el
|
|
95
|
+
* pivote lo pliega quien guarda los datos y esto no se usa: la grilla recibe un
|
|
96
|
+
* `Pivot` de `pivotOf` y dibuja igual, porque dibuja el mismo tipo.
|
|
97
|
+
*
|
|
98
|
+
* **Los márgenes se pliegan sobre las filas de la base y no sobre los cruces**,
|
|
99
|
+
* que es la regla 2 escrita en código: el margen de «Ferretería Sur» pliega sus 187
|
|
100
|
+
* facturas de una, no los doce números mensuales que ya se dibujaron. Es
|
|
101
|
+
* lo único que hace que una medida que no sea una suma —un promedio, una
|
|
102
|
+
* mediana, un distintos— diga la verdad en el borde.
|
|
103
|
+
*/
|
|
104
|
+
export function pivotFrom(items, spec) {
|
|
105
|
+
const label = (level) => spec.labelOf?.(level) ?? level.field;
|
|
106
|
+
const rowBuckets = axisBuckets(items, spec.rows, spec.of);
|
|
107
|
+
const columnBuckets = axisBuckets(items, spec.columns, spec.of);
|
|
108
|
+
const crossings = new Map();
|
|
109
|
+
for (const row of rowBuckets.values()) {
|
|
110
|
+
const inside = axisBuckets(row.items, spec.columns, spec.of);
|
|
111
|
+
for (const [columnId, column] of inside) {
|
|
112
|
+
crossings.set(`${pathKey(row.key)}${CROSS}${columnId}`, fold(spec.measures, column.items));
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const rowMargins = new Map();
|
|
116
|
+
for (const [id, row] of rowBuckets)
|
|
117
|
+
rowMargins.set(id, fold(spec.measures, row.items));
|
|
118
|
+
const columnMargins = new Map();
|
|
119
|
+
for (const [id, column] of columnBuckets) {
|
|
120
|
+
columnMargins.set(id, fold(spec.measures, column.items));
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
rows: {
|
|
124
|
+
levels: spec.rows.map((one) => ({ ...one, label: label(one) })),
|
|
125
|
+
keys: [...rowBuckets.values()].map((one) => one.key),
|
|
126
|
+
},
|
|
127
|
+
columns: {
|
|
128
|
+
levels: spec.columns.map((one) => ({ ...one, label: label(one) })),
|
|
129
|
+
keys: [...columnBuckets.values()].map((one) => one.key),
|
|
130
|
+
},
|
|
131
|
+
measures: spec.measures.map((one) => ({
|
|
132
|
+
id: one.id,
|
|
133
|
+
header: one.header,
|
|
134
|
+
...(one.format === undefined ? {} : { format: one.format }),
|
|
135
|
+
...(one.numberFormat === undefined ? {} : { numberFormat: one.numberFormat }),
|
|
136
|
+
})),
|
|
137
|
+
cell: (row, column) => crossings.get(crossKey(row, column)),
|
|
138
|
+
rowTotal: (row) => rowMargins.get(pathKey(row)),
|
|
139
|
+
columnTotal: (column) => columnMargins.get(pathKey(column)),
|
|
140
|
+
total: fold(spec.measures, items),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
/** Si el pivote no tiene ni una fila que cruzar. Un eje vacío no es un pivote
|
|
144
|
+
* chico: es la vista vacía, y se dice con la frase del conjunto y no con una
|
|
145
|
+
* grilla de un renglón. */
|
|
146
|
+
export function pivotIsEmpty(pivot) {
|
|
147
|
+
return pivot.rows.keys.length === 0 || pivot.columns.keys.length === 0;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Los tramos de una clave que hay que ESCRIBIR, comparada con la anterior.
|
|
151
|
+
*
|
|
152
|
+
* En el área de filas de un pivote, el valor de un nivel se escribe sólo cuando
|
|
153
|
+
* cambia: repetir «Ferretería Sur» en las doce filas de sus doce meses convierte la
|
|
154
|
+
* jerarquía en una columna de ruido y hace desaparecer justamente el corte que
|
|
155
|
+
* se pidió. Devuelve, por nivel, si ese tramo arranca acá.
|
|
156
|
+
*/
|
|
157
|
+
export function keyStarts(key, previous) {
|
|
158
|
+
let broken = previous === undefined;
|
|
159
|
+
return key.map((part, level) => {
|
|
160
|
+
if (broken)
|
|
161
|
+
return true;
|
|
162
|
+
/* Una vez que un nivel de arriba cambió, todos los de abajo arrancan de
|
|
163
|
+
nuevo aunque digan lo mismo: el «2026-09» de «Distribuidora Andina» no es la
|
|
164
|
+
continuación del «2026-09» de «Ferretería Sur». */
|
|
165
|
+
if (part !== previous?.[level]) {
|
|
166
|
+
broken = true;
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
return false;
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Cuánto abarca cada tramo del eje horizontal, nivel por nivel.
|
|
174
|
+
*
|
|
175
|
+
* El encabezado de un pivote es una jerarquía acostada: un renglón por nivel, y
|
|
176
|
+
* cada rótulo cubriendo las columnas que cuelgan de él. Esto lo resuelve una vez
|
|
177
|
+
* —de a tramos consecutivos iguales, que es lo que hace que «2026» cubra sus
|
|
178
|
+
* cuatro trimestres— y la grilla sólo lo dibuja.
|
|
179
|
+
*/
|
|
180
|
+
export function axisSpans(axis, level) {
|
|
181
|
+
const keys = axis.keys;
|
|
182
|
+
const out = [];
|
|
183
|
+
keys.forEach((key, at) => {
|
|
184
|
+
const label = axisLabels(axis, key)[level] ?? "";
|
|
185
|
+
const last = out[out.length - 1];
|
|
186
|
+
/* Se compara el PREFIJO entero y no el rótulo: dos «2026-09» debajo de dos
|
|
187
|
+
clientes distintos son dos tramos, y fundirlos en uno dibujaría una
|
|
188
|
+
celda que abarca columnas que no son hermanas. */
|
|
189
|
+
const samePrefix = last !== undefined &&
|
|
190
|
+
last.label === label &&
|
|
191
|
+
key.slice(0, level).join(CROSS) === (keys[at - 1]?.slice(0, level).join(CROSS) ?? null);
|
|
192
|
+
if (samePrefix)
|
|
193
|
+
last.span += 1;
|
|
194
|
+
else
|
|
195
|
+
out.push({ label, span: 1, at });
|
|
196
|
+
});
|
|
197
|
+
return out;
|
|
198
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { type Pivot } from "./pivot.js";
|
|
2
|
+
import { type XlsxSheet } from "./xlsx.js";
|
|
3
|
+
/**
|
|
4
|
+
* EL PIVOTE COMO PLANILLA: la misma grilla, en un archivo.
|
|
5
|
+
*
|
|
6
|
+
* ── Por qué esto existe y no alcanza con «exportar la tabla» ────────
|
|
7
|
+
*
|
|
8
|
+
* Una grilla que sale a Excel como texto plano pierde las tres cosas que la
|
|
9
|
+
* hacían una grilla: el encabezado deja de ser una jerarquía, los números dejan
|
|
10
|
+
* de sumarse y los agujeros se vuelven ceros. Las tres se arreglan acá:
|
|
11
|
+
*
|
|
12
|
+
* el encabezado va con celdas COMBINADAS, así «2026» sigue cubriendo sus
|
|
13
|
+
* cuatro trimestres
|
|
14
|
+
* los números entran como números, con su formato — no como texto que
|
|
15
|
+
* parece un número y no se puede sumar
|
|
16
|
+
* los agujeros no se escriben. Una celda ausente en el archivo es lo
|
|
17
|
+
* mismo que un cruce sin filas en la pantalla
|
|
18
|
+
*
|
|
19
|
+
* Y los rótulos quedan congelados, por la misma razón que en la pantalla se
|
|
20
|
+
* quedan pegados.
|
|
21
|
+
*
|
|
22
|
+
* ── Lo que el archivo NO puede decir ────────────────────────────────
|
|
23
|
+
*
|
|
24
|
+
* En la grilla, un cruce vacío y una medida que no aplica se dibujan distinto:
|
|
25
|
+
* blanco el primero, un punto el segundo. En la planilla los dos son una celda
|
|
26
|
+
* vacía, y es deliberado — un punto adentro de una columna de números la
|
|
27
|
+
* convierte en una columna de texto, y ahí se pierde poder sumarla, que es para
|
|
28
|
+
* lo que alguien baja una planilla.
|
|
29
|
+
*
|
|
30
|
+
* ── Lo que esto exporta es lo que está en la mano ───────────────────
|
|
31
|
+
*
|
|
32
|
+
* Que es exactamente todo, y por eso el pivote es la presentación donde exportar
|
|
33
|
+
* desde el navegador no miente: la grilla entera ya bajó —son rótulos y números
|
|
34
|
+
* plegados, no los registros— así que el archivo dice lo mismo que la pantalla.
|
|
35
|
+
* Una lista paginada no tiene esa suerte y su exportación es otra conversación.
|
|
36
|
+
*/
|
|
37
|
+
export type PivotSheetOptions = {
|
|
38
|
+
/** El nombre de la pestaña, y del archivo cuando se baja. */
|
|
39
|
+
name?: string;
|
|
40
|
+
/** Un renglón arriba de todo que dice qué es esta grilla. */
|
|
41
|
+
caption?: string;
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* La grilla, como hoja de cálculo.
|
|
45
|
+
*
|
|
46
|
+
* Devuelve la FORMA y no el archivo: los mismos datos sirven para bajarlos acá,
|
|
47
|
+
* para mandarlos a un servidor que escriba el libro con más hojas, o para
|
|
48
|
+
* mirarlos en una prueba sin descomprimir nada.
|
|
49
|
+
*/
|
|
50
|
+
export declare function pivotSheet(pivot: Pivot, options?: PivotSheetOptions): XlsxSheet;
|
|
51
|
+
/** Los bytes del `.xlsx` de una grilla. */
|
|
52
|
+
export declare function pivotXlsx(pivot: Pivot, options?: PivotSheetOptions): Uint8Array;
|
|
53
|
+
/**
|
|
54
|
+
* Bajar la grilla como planilla, en una línea.
|
|
55
|
+
*
|
|
56
|
+
* Existe para que el que la usa no vuelva a escribir el armado: tres consumidores
|
|
57
|
+
* encadenando `pivotSheet → writeXlsx → downloadXlsx` a mano son tres lugares
|
|
58
|
+
* donde alguien se olvida del título o del nombre del archivo, y entonces la
|
|
59
|
+
* misma grilla baja distinta según de qué pantalla salió.
|
|
60
|
+
*/
|
|
61
|
+
export declare function downloadPivot(pivot: Pivot, options?: PivotSheetOptions): void;
|
|
Binary file
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* LAS RANURAS — cómo un arquetipo se escribe como ÁRBOL.
|
|
4
|
+
*
|
|
5
|
+
* Context: la forma de una pantalla es opcional de una manera que una lista de
|
|
6
|
+
* props no sabe decir. ¿Hay riel? ¿Hay pestañas? ¿Hay paginador? ¿Esta lista se
|
|
7
|
+
* puede ver como tabla? Una lista de props contesta eso con `pager={undefined}`
|
|
8
|
+
* y con una segunda prop que declara qué caras se ofrecen — dos lugares que se
|
|
9
|
+
* pueden contradecir. Un árbol lo dice en voz alta: **si no está escrito, no
|
|
10
|
+
* está.**
|
|
11
|
+
*
|
|
12
|
+
* <Record>
|
|
13
|
+
* <Record.Rail>
|
|
14
|
+
* <Record.Pager index={12} total={80} /> ← hay paginador
|
|
15
|
+
* </Record.Rail>
|
|
16
|
+
* </Record>
|
|
17
|
+
*
|
|
18
|
+
* ── Por qué una MARCA y no la identidad del componente ──────────────
|
|
19
|
+
*
|
|
20
|
+
* Reconocer una ranura comparando `node.type === Record.Pager` falla en
|
|
21
|
+
* silencio el día que hay dos copias del módulo en memoria —recarga en caliente,
|
|
22
|
+
* un bundle mal deduplicado— y entonces el árbol se dibuja vacío sin un solo
|
|
23
|
+
* error. La marca viaja con el componente y sobrevive a eso.
|
|
24
|
+
*
|
|
25
|
+
* ── Esto vive en `system/` y no adentro de un arquetipo ─────────────
|
|
26
|
+
*
|
|
27
|
+
* Porque lo hablan todos los que se escriben como árbol. Mientras estuvo
|
|
28
|
+
* escondido adentro de `Record.tsx`, ser un árbol era un privilegio de los
|
|
29
|
+
* registros: cualquier otro arquetipo tenía que copiar el mecanismo o quedarse
|
|
30
|
+
* con treinta y seis props.
|
|
31
|
+
*/
|
|
32
|
+
/** Un componente marcado como ranura. La marca es el nombre, así que un árbol la
|
|
33
|
+
* lee sin saber de dónde salió el componente. */
|
|
34
|
+
export type Slotted<Name extends string> = {
|
|
35
|
+
__slot?: Name;
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Marca un componente como ranura y lo devuelve.
|
|
39
|
+
*
|
|
40
|
+
* El componente NO dibuja nada: existe para ser leído por el arquetipo, que es
|
|
41
|
+
* quien decide dónde va lo que trae adentro. Devolver `null` es lo correcto —
|
|
42
|
+
* una ranura que se dibujara sola aparecería dos veces, una donde la escribieron
|
|
43
|
+
* y otra donde el arquetipo la puso.
|
|
44
|
+
*/
|
|
45
|
+
export declare function slot<Name extends string, P>(name: Name, component: (props: P) => ReactNode): ((props: P) => ReactNode) & Slotted<Name>;
|
|
46
|
+
/** Qué ranura es un nodo, si es alguna. */
|
|
47
|
+
export declare function slotOf<Name extends string>(node: ReactNode): Name | undefined;
|
|
48
|
+
/**
|
|
49
|
+
* Los hijos, con los fragmentos abiertos.
|
|
50
|
+
*
|
|
51
|
+
* `Children.toArray` aplana arreglos pero NO fragmentos, y un `<>…</>` en el
|
|
52
|
+
* medio del árbol —lo que escribe cualquiera que ponga dos ranuras bajo un
|
|
53
|
+
* condicional— escondería las ranuras que tiene adentro.
|
|
54
|
+
*/
|
|
55
|
+
export declare function flatten(children: ReactNode): ReactNode[];
|
|
56
|
+
/** La primera ranura de ese nombre. */
|
|
57
|
+
export declare function pick<Name extends string>(nodes: readonly ReactNode[], name: Name): ReactNode | undefined;
|
|
58
|
+
/** Todas las ranuras de ese nombre, en el orden en que se escribieron. Lo usa el
|
|
59
|
+
* que acepta varias hermanas —las páginas de un cuaderno. */
|
|
60
|
+
export declare function pickAll<Name extends string>(nodes: readonly ReactNode[], name: Name): ReactNode[];
|
|
61
|
+
/** Las props con las que se escribió una ranura. Es lo único que el arquetipo
|
|
62
|
+
* necesita de ella: la ranura no dibuja, declara. */
|
|
63
|
+
export declare function propsOf<P>(node: ReactNode | undefined): P | undefined;
|
|
64
|
+
/** Lo que NO es ninguna de esas ranuras. Es la escotilla: lo que el arquetipo no
|
|
65
|
+
* previó se dibuja igual, y que aparezca algo acá es la señal de que al
|
|
66
|
+
* arquetipo le falta una pieza. */
|
|
67
|
+
export declare function loose<Name extends string>(nodes: readonly ReactNode[], names: readonly Name[]): ReactNode[];
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { Children, Fragment, isValidElement } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* Marca un componente como ranura y lo devuelve.
|
|
4
|
+
*
|
|
5
|
+
* El componente NO dibuja nada: existe para ser leído por el arquetipo, que es
|
|
6
|
+
* quien decide dónde va lo que trae adentro. Devolver `null` es lo correcto —
|
|
7
|
+
* una ranura que se dibujara sola aparecería dos veces, una donde la escribieron
|
|
8
|
+
* y otra donde el arquetipo la puso.
|
|
9
|
+
*/
|
|
10
|
+
export function slot(name, component) {
|
|
11
|
+
const marked = component;
|
|
12
|
+
marked.__slot = name;
|
|
13
|
+
return marked;
|
|
14
|
+
}
|
|
15
|
+
/** Qué ranura es un nodo, si es alguna. */
|
|
16
|
+
export function slotOf(node) {
|
|
17
|
+
if (!isValidElement(node))
|
|
18
|
+
return undefined;
|
|
19
|
+
const type = node.type;
|
|
20
|
+
return typeof type === "string" ? undefined : type.__slot;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Los hijos, con los fragmentos abiertos.
|
|
24
|
+
*
|
|
25
|
+
* `Children.toArray` aplana arreglos pero NO fragmentos, y un `<>…</>` en el
|
|
26
|
+
* medio del árbol —lo que escribe cualquiera que ponga dos ranuras bajo un
|
|
27
|
+
* condicional— escondería las ranuras que tiene adentro.
|
|
28
|
+
*/
|
|
29
|
+
export function flatten(children) {
|
|
30
|
+
const out = [];
|
|
31
|
+
for (const child of Children.toArray(children)) {
|
|
32
|
+
if (isValidElement(child) && child.type === Fragment) {
|
|
33
|
+
out.push(...flatten(child.props.children));
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
out.push(child);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
/** La primera ranura de ese nombre. */
|
|
42
|
+
export function pick(nodes, name) {
|
|
43
|
+
return nodes.find((node) => slotOf(node) === name);
|
|
44
|
+
}
|
|
45
|
+
/** Todas las ranuras de ese nombre, en el orden en que se escribieron. Lo usa el
|
|
46
|
+
* que acepta varias hermanas —las páginas de un cuaderno. */
|
|
47
|
+
export function pickAll(nodes, name) {
|
|
48
|
+
return nodes.filter((node) => slotOf(node) === name);
|
|
49
|
+
}
|
|
50
|
+
/** Las props con las que se escribió una ranura. Es lo único que el arquetipo
|
|
51
|
+
* necesita de ella: la ranura no dibuja, declara. */
|
|
52
|
+
export function propsOf(node) {
|
|
53
|
+
return isValidElement(node) ? node.props : undefined;
|
|
54
|
+
}
|
|
55
|
+
/** Lo que NO es ninguna de esas ranuras. Es la escotilla: lo que el arquetipo no
|
|
56
|
+
* previó se dibuja igual, y que aparezca algo acá es la señal de que al
|
|
57
|
+
* arquetipo le falta una pieza. */
|
|
58
|
+
export function loose(nodes, names) {
|
|
59
|
+
return nodes.filter((node) => {
|
|
60
|
+
const name = slotOf(node);
|
|
61
|
+
return name === undefined || !names.includes(name);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { type CSSProperties, type RefObject } from "react";
|
|
2
|
+
/** La variable que dice a qué altura arranca lo próximo que se pegue. */
|
|
3
|
+
export declare const STICKY_TOP = "--sticky-top";
|
|
4
|
+
/** Lo que va en `style` de algo pegajoso: se pega debajo de lo que ya hay. */
|
|
5
|
+
export declare const stickyTop: CSSProperties;
|
|
6
|
+
/** Para una caja con scroll propio: adentro, la pila arranca de cero. */
|
|
7
|
+
export declare function stickyReset(): CSSProperties;
|
|
8
|
+
/**
|
|
9
|
+
* LA CAJA DE UNA TABLA: cuándo tiene que ser un contenedor de scroll y cuándo no.
|
|
10
|
+
*
|
|
11
|
+
* Las tres tablas del sistema tienen el mismo dilema y lo resolvían con tres
|
|
12
|
+
* copias de la misma medición. El dilema es de CSS y no se puede esquivar:
|
|
13
|
+
* `overflow-x: auto` **obliga** a `overflow-y: auto` —la especificación dice que
|
|
14
|
+
* `visible` junto a un valor que no sea `visible` ni `clip` computa a `auto`—,
|
|
15
|
+
* así que en cuanto la caja permite scroll horizontal pasa a ser el scroller, y
|
|
16
|
+
* el encabezado pegajoso se pega a SU borde en vez de al de la página.
|
|
17
|
+
*
|
|
18
|
+
* De ahí salen los tres casos, y son los únicos:
|
|
19
|
+
*
|
|
20
|
+
* alto propio la caja scrollea de verdad. `auto`, y la pila de cosas
|
|
21
|
+
* pegadas arranca de cero adentro.
|
|
22
|
+
* sin alto, entra no hace falta ninguna caja. `clip` —recorta las esquinas
|
|
23
|
+
* redondeadas sin ser un scroller— y el encabezado se pega
|
|
24
|
+
* a la página, debajo de la barra de la lista.
|
|
25
|
+
* sin alto, no entra vuelve el scroll horizontal, y el encabezado se va
|
|
26
|
+
* con las filas. Es el precio, y es el que hay.
|
|
27
|
+
*
|
|
28
|
+
* Devuelve la clase y el estilo ya resueltos para que ninguna tabla vuelva a
|
|
29
|
+
* decidirlo por su cuenta.
|
|
30
|
+
*/
|
|
31
|
+
export declare function useScrollBox<T extends HTMLElement>(options: {
|
|
32
|
+
/** El alto declarado. `undefined` o `"auto"` = crece con el contenido. */
|
|
33
|
+
height?: number | string;
|
|
34
|
+
}): {
|
|
35
|
+
ref: RefObject<T | null>;
|
|
36
|
+
className: string;
|
|
37
|
+
style: CSSProperties;
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Mide el alto de un elemento y devuelve el estilo que empuja la pila para sus
|
|
41
|
+
* hermanos de abajo.
|
|
42
|
+
*
|
|
43
|
+
* `useLayoutEffect` y `ResizeObserver` y no un alto declarado: el alto depende
|
|
44
|
+
* de si las fichas del buscador envolvieron, y eso recién se sabe después de
|
|
45
|
+
* pintar.
|
|
46
|
+
*/
|
|
47
|
+
export declare function useStickyLayer<T extends HTMLElement>(): {
|
|
48
|
+
ref: RefObject<T | null>;
|
|
49
|
+
/** Va en el elemento que se pega. */
|
|
50
|
+
self: CSSProperties;
|
|
51
|
+
/** Va en lo que viene DEBAJO, para que se pegue más abajo todavía. */
|
|
52
|
+
below: CSSProperties;
|
|
53
|
+
};
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useLayoutEffect, useRef, useState } from "react";
|
|
3
|
+
/* ==================================================================== *
|
|
4
|
+
* LA PILA DE COSAS PEGADAS
|
|
5
|
+
*
|
|
6
|
+
* Tres bandas se quedan arriba mientras se scrollea una vista larga, y cada
|
|
7
|
+
* una tiene que quedar DEBAJO de la anterior:
|
|
8
|
+
*
|
|
9
|
+
* ┌─ TopNav ─────────────────────────────────┐ top: 0
|
|
10
|
+
* ├─ barra de la lista ──────────────────────┤ top: alto del TopNav
|
|
11
|
+
* │ 🔍 buscar… ⚙3 1–80 / 187 ‹ › │
|
|
12
|
+
* ├─ encabezado de la tabla ─────────────────┤ top: los dos de arriba
|
|
13
|
+
* │ número │ cliente │ fecha │ total │
|
|
14
|
+
* ├──────────────────────────────────────────┤
|
|
15
|
+
* │ …las filas, que sí se van… │
|
|
16
|
+
*
|
|
17
|
+
* **Ninguna de las tres tiene alto fijo.** El `TopNav` crece cuando envuelve;
|
|
18
|
+
* la barra de la lista tiene dos renglones, o uno en compacto, o tres cuando
|
|
19
|
+
* las fichas del buscador envuelven. Un `top-[52px]` escrito a mano acierta en
|
|
20
|
+
* una ventana y deja un hueco o un solapamiento en las otras — y es la clase
|
|
21
|
+
* de número que nadie vuelve a tocar cuando la barra cambia.
|
|
22
|
+
*
|
|
23
|
+
* Por eso cada banda MIDE lo que ocupa y lo suma a `--sticky-top` para lo que
|
|
24
|
+
* venga abajo. La variable cascadea, así que cada capa lee la de arriba sin
|
|
25
|
+
* que ninguna sepa cuántas capas hay.
|
|
26
|
+
*
|
|
27
|
+
* ── La regla del que abre una caja con scroll propio ────────────────
|
|
28
|
+
*
|
|
29
|
+
* Adentro de una caja con alto y `overflow`, el que scrollea es la caja y no
|
|
30
|
+
* la página, así que lo pegado se pega al borde de la caja: ahí la pila
|
|
31
|
+
* arranca de cero otra vez. Eso es `stickyReset()`.
|
|
32
|
+
* ==================================================================== */
|
|
33
|
+
/** La variable que dice a qué altura arranca lo próximo que se pegue. */
|
|
34
|
+
export const STICKY_TOP = "--sticky-top";
|
|
35
|
+
/** Lo que va en `style` de algo pegajoso: se pega debajo de lo que ya hay. */
|
|
36
|
+
export const stickyTop = { top: `var(${STICKY_TOP}, 0px)` };
|
|
37
|
+
/** Para una caja con scroll propio: adentro, la pila arranca de cero. */
|
|
38
|
+
export function stickyReset() {
|
|
39
|
+
return { [STICKY_TOP]: "0px" };
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* LA CAJA DE UNA TABLA: cuándo tiene que ser un contenedor de scroll y cuándo no.
|
|
43
|
+
*
|
|
44
|
+
* Las tres tablas del sistema tienen el mismo dilema y lo resolvían con tres
|
|
45
|
+
* copias de la misma medición. El dilema es de CSS y no se puede esquivar:
|
|
46
|
+
* `overflow-x: auto` **obliga** a `overflow-y: auto` —la especificación dice que
|
|
47
|
+
* `visible` junto a un valor que no sea `visible` ni `clip` computa a `auto`—,
|
|
48
|
+
* así que en cuanto la caja permite scroll horizontal pasa a ser el scroller, y
|
|
49
|
+
* el encabezado pegajoso se pega a SU borde en vez de al de la página.
|
|
50
|
+
*
|
|
51
|
+
* De ahí salen los tres casos, y son los únicos:
|
|
52
|
+
*
|
|
53
|
+
* alto propio la caja scrollea de verdad. `auto`, y la pila de cosas
|
|
54
|
+
* pegadas arranca de cero adentro.
|
|
55
|
+
* sin alto, entra no hace falta ninguna caja. `clip` —recorta las esquinas
|
|
56
|
+
* redondeadas sin ser un scroller— y el encabezado se pega
|
|
57
|
+
* a la página, debajo de la barra de la lista.
|
|
58
|
+
* sin alto, no entra vuelve el scroll horizontal, y el encabezado se va
|
|
59
|
+
* con las filas. Es el precio, y es el que hay.
|
|
60
|
+
*
|
|
61
|
+
* Devuelve la clase y el estilo ya resueltos para que ninguna tabla vuelva a
|
|
62
|
+
* decidirlo por su cuenta.
|
|
63
|
+
*/
|
|
64
|
+
export function useScrollBox(options) {
|
|
65
|
+
const ref = useRef(null);
|
|
66
|
+
const crece = options.height === undefined || options.height === "auto";
|
|
67
|
+
const [desborda, setDesborda] = useState(false);
|
|
68
|
+
useLayoutEffect(() => {
|
|
69
|
+
const node = ref.current;
|
|
70
|
+
if (node === null || !crece)
|
|
71
|
+
return;
|
|
72
|
+
/* Se mide el CONTENIDO y no la caja, y es la diferencia entre que esto
|
|
73
|
+
funcione y que se trabe: con `overflow: clip` la caja deja de ser un
|
|
74
|
+
contenedor de scroll, así que su `scrollWidth` pasa a ser su propio ancho
|
|
75
|
+
y nunca volvería a reportar desborde. Una vez recortada, se quedaría
|
|
76
|
+
recortada para siempre. El hijo sí sabe cuánto mide de verdad. */
|
|
77
|
+
const dentro = node.firstElementChild;
|
|
78
|
+
const medir = () => {
|
|
79
|
+
const ancho = dentro === null ? node.scrollWidth : dentro.getBoundingClientRect().width;
|
|
80
|
+
setDesborda(ancho > node.clientWidth + 1);
|
|
81
|
+
};
|
|
82
|
+
medir();
|
|
83
|
+
if (typeof ResizeObserver === "undefined")
|
|
84
|
+
return;
|
|
85
|
+
const observer = new ResizeObserver(medir);
|
|
86
|
+
observer.observe(node);
|
|
87
|
+
/* También el contenido: las columnas cambian de ancho sin que la caja
|
|
88
|
+
cambie de tamaño. */
|
|
89
|
+
if (dentro !== null)
|
|
90
|
+
observer.observe(dentro);
|
|
91
|
+
return () => observer.disconnect();
|
|
92
|
+
}, [crece]);
|
|
93
|
+
const suelta = crece && !desborda;
|
|
94
|
+
return {
|
|
95
|
+
ref,
|
|
96
|
+
className: suelta ? "overflow-clip" : "overflow-auto",
|
|
97
|
+
style: suelta
|
|
98
|
+
? {}
|
|
99
|
+
: {
|
|
100
|
+
...(crece || options.height === undefined ? {} : { height: options.height }),
|
|
101
|
+
...stickyReset(),
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Mide el alto de un elemento y devuelve el estilo que empuja la pila para sus
|
|
107
|
+
* hermanos de abajo.
|
|
108
|
+
*
|
|
109
|
+
* `useLayoutEffect` y `ResizeObserver` y no un alto declarado: el alto depende
|
|
110
|
+
* de si las fichas del buscador envolvieron, y eso recién se sabe después de
|
|
111
|
+
* pintar.
|
|
112
|
+
*/
|
|
113
|
+
export function useStickyLayer() {
|
|
114
|
+
const ref = useRef(null);
|
|
115
|
+
const [top, setTop] = useState(0);
|
|
116
|
+
useLayoutEffect(() => {
|
|
117
|
+
const node = ref.current;
|
|
118
|
+
if (node === null)
|
|
119
|
+
return;
|
|
120
|
+
/* El valor de abajo se calcula en JS y se escribe en PÍXELES, no como
|
|
121
|
+
`calc(var(--sticky-top) + 44px)`. Context: esa forma parece la natural y
|
|
122
|
+
no funciona — poner `--sticky-top` en un elemento leyendo `var(--sticky-top)`
|
|
123
|
+
del MISMO elemento es un ciclo, CSS lo declara inválido, y los
|
|
124
|
+
descendientes heredan «nada»: todo se pegaba en 0 y las bandas se
|
|
125
|
+
superponían. Se midió en el navegador, no se dedujo. */
|
|
126
|
+
const measure = () => {
|
|
127
|
+
const inherited = Number.parseFloat(getComputedStyle(node).getPropertyValue(STICKY_TOP));
|
|
128
|
+
const above = Number.isFinite(inherited) ? inherited : 0;
|
|
129
|
+
setTop(above + node.getBoundingClientRect().height);
|
|
130
|
+
};
|
|
131
|
+
measure();
|
|
132
|
+
if (typeof ResizeObserver === "undefined")
|
|
133
|
+
return;
|
|
134
|
+
const observer = new ResizeObserver(measure);
|
|
135
|
+
observer.observe(node);
|
|
136
|
+
/* La altura de una capa de ARRIBA cambia sin que este observador se entere
|
|
137
|
+
—el `TopNav` envuelve al angostar—, así que también se remide al cambiar
|
|
138
|
+
el tamaño de la ventana, que es cuando eso pasa. */
|
|
139
|
+
window.addEventListener("resize", measure);
|
|
140
|
+
return () => {
|
|
141
|
+
observer.disconnect();
|
|
142
|
+
window.removeEventListener("resize", measure);
|
|
143
|
+
};
|
|
144
|
+
}, []);
|
|
145
|
+
return {
|
|
146
|
+
ref,
|
|
147
|
+
self: stickyTop,
|
|
148
|
+
below: { [STICKY_TOP]: `${top}px` },
|
|
149
|
+
};
|
|
150
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Surface } from "./types.js";
|
|
2
|
+
import { type ReactNode } from "react";
|
|
3
|
+
export declare function SurfaceProvider({ value, children, }: {
|
|
4
|
+
value: Surface;
|
|
5
|
+
children: ReactNode;
|
|
6
|
+
}): import("react").JSX.Element;
|
|
7
|
+
export declare function useSurface(explicit?: Surface): Surface;
|
|
8
|
+
/** Picks between two values according to the surface. The twin of `byDensity`. */
|
|
9
|
+
export declare const bySurface: <T>(surface: Surface, canvas: T, chrome: T) => T;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { createContext, useContext } from "react";
|
|
4
|
+
/**
|
|
5
|
+
* Context: which surface whatever is inside is drawing on.
|
|
6
|
+
*
|
|
7
|
+
* It is a context and not a prop —the opposite of `Button`— for the same reason
|
|
8
|
+
* as density: a surface has to be UNIFORM inside. A chrome bar with a canvas
|
|
9
|
+
* search box inside is not a mix, it is a bug, and that happened: the status bar
|
|
10
|
+
* was drawn with the canvas palette on top of the navy because whoever put it
|
|
11
|
+
* there forgot to pass `surface="chrome"`.
|
|
12
|
+
*
|
|
13
|
+
* The pieces that live on both still accept it as a prop: the prop wins, and the
|
|
14
|
+
* context is only the default value. That way a one-off decision can be audited
|
|
15
|
+
* by reading the JSX, which is the system's rule for color.
|
|
16
|
+
*/
|
|
17
|
+
const Ctx = createContext("canvas");
|
|
18
|
+
export function SurfaceProvider({ value, children, }) {
|
|
19
|
+
return _jsx(Ctx.Provider, { value: value, children: children });
|
|
20
|
+
}
|
|
21
|
+
export function useSurface(explicit) {
|
|
22
|
+
const inherited = useContext(Ctx);
|
|
23
|
+
return explicit ?? inherited;
|
|
24
|
+
}
|
|
25
|
+
/** Picks between two values according to the surface. The twin of `byDensity`. */
|
|
26
|
+
export const bySurface = (surface, canvas, chrome) => surface === "chrome" ? chrome : canvas;
|