@lerianstudio/sindarian-ui 2.0.0-beta.24 → 2.0.0-beta.26
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/dist/charts/chart.js +4 -231
- package/dist/charts/index.js +1 -11
- package/dist/components/breadcrumb/get-breadcrumb-paths.js +1 -18
- package/dist/components/breadcrumb/index.js +1 -37
- package/dist/components/card/account-card/index.js +1 -69
- package/dist/components/card/basic-card/index.js +1 -20
- package/dist/components/card/dashboard-card/index.js +1 -20
- package/dist/components/card/entity-card/index.js +1 -68
- package/dist/components/card/index.js +1 -20
- package/dist/components/dialog/confirmation-dialog/index.d.ts.map +1 -1
- package/dist/components/dialog/confirmation-dialog/index.js +1 -96
- package/dist/components/dialog/confirmation-dialog/use-confirm-dialog.js +1 -55
- package/dist/components/dialog/index.js +1 -19
- package/dist/components/dialog/large-dialog/index.d.ts +2 -2
- package/dist/components/dialog/large-dialog/index.d.ts.map +1 -1
- package/dist/components/dialog/large-dialog/index.js +1 -21
- package/dist/components/entity-box/index.js +1 -67
- package/dist/components/entity-data-table/index.js +1 -20
- package/dist/components/form/combo-box-field/index.js +1 -35
- package/dist/components/form/country-field/index.js +1 -11
- package/dist/components/form/date-picker-field/index.js +1 -83
- package/dist/components/form/date-range-field/index.js +1 -58
- package/dist/components/form/date-time-picker-field/index.js +1 -114
- package/dist/components/form/file-upload-field/index.js +1 -69
- package/dist/components/form/index.js +1 -31
- package/dist/components/form/input-field/index.js +1 -96
- package/dist/components/form/otp-field/index.js +1 -19
- package/dist/components/form/pagination-limit-field/index.js +1 -10
- package/dist/components/form/password-field/index.d.ts.map +1 -1
- package/dist/components/form/password-field/index.js +1 -16
- package/dist/components/form/radio-group-field/index.js +1 -36
- package/dist/components/form/renderable-label.js +1 -57
- package/dist/components/form/select-field/index.js +1 -68
- package/dist/components/form/state-field/index.js +1 -42
- package/dist/components/form/switch-field/index.js +1 -16
- package/dist/components/form/textarea-field/index.js +1 -23
- package/dist/components/page/index.js +1 -20
- package/dist/components/page-header/index.js +1 -80
- package/dist/components/pagination/index.js +1 -10
- package/dist/components/table/id-table-cell.js +1 -109
- package/dist/components/table/index.js +1 -19
- package/dist/components/table/locked-table-actions.js +1 -41
- package/dist/components/table/name-table-cell.js +1 -58
- package/dist/components/ui/accordion/index.js +1 -62
- package/dist/components/ui/alert/index.js +1 -45
- package/dist/components/ui/alert-dialog/index.js +1 -84
- package/dist/components/ui/autocomplete/index.js +1 -440
- package/dist/components/ui/autosize-textarea/index.js +1 -89
- package/dist/components/ui/avatar/index.js +1 -51
- package/dist/components/ui/badge/index.js +1 -83
- package/dist/components/ui/breadcrumb/index.js +1 -35
- package/dist/components/ui/button/index.js +1 -65
- package/dist/components/ui/button/styles.css +2 -1
- package/dist/components/ui/calendar/index.js +1 -118
- package/dist/components/ui/card/index.js +1 -32
- package/dist/components/ui/checkbox/index.js +1 -44
- package/dist/components/ui/collapsible/index.js +1 -50
- package/dist/components/ui/command/index.js +1 -95
- package/dist/components/ui/copy-field/index.js +1 -163
- package/dist/components/ui/dialog/index.d.ts +4 -1
- package/dist/components/ui/dialog/index.d.ts.map +1 -1
- package/dist/components/ui/dialog/index.js +1 -80
- package/dist/components/ui/dialog/styles.css +2 -1
- package/dist/components/ui/dropdown-menu/index.js +1 -110
- package/dist/components/ui/field/index.js +1 -82
- package/dist/components/ui/file-upload/index.js +1 -574
- package/dist/components/ui/form.js +1 -162
- package/dist/components/ui/hover-card/index.js +1 -51
- package/dist/components/ui/icon-button/index.js +1 -35
- package/dist/components/ui/input/adornment.js +1 -21
- package/dist/components/ui/input/index.js +1 -99
- package/dist/components/ui/input/styles.css +29 -12
- package/dist/components/ui/label/index.js +1 -45
- package/dist/components/ui/lerian-wordmark/index.js +1 -7
- package/dist/components/ui/loading-button/index.js +1 -9
- package/dist/components/ui/multiple-select/index.js +1 -235
- package/dist/components/ui/otp-input/index.js +1 -62
- package/dist/components/ui/paper/index.js +1 -8
- package/dist/components/ui/paper-collapsible/index.js +1 -24
- package/dist/components/ui/popover/index.js +1 -55
- package/dist/components/ui/progress/index.js +1 -66
- package/dist/components/ui/qr-code/index.js +1 -27
- package/dist/components/ui/radio-group/index.js +1 -48
- package/dist/components/ui/scroll-area/index.js +1 -49
- package/dist/components/ui/select/index.js +1 -119
- package/dist/components/ui/select/styles.css +6 -3
- package/dist/components/ui/separator/index.js +1 -43
- package/dist/components/ui/sheet/index.d.ts +7 -2
- package/dist/components/ui/sheet/index.d.ts.map +1 -1
- package/dist/components/ui/sheet/index.js +1 -136
- package/dist/components/ui/sidebar/index.js +1 -27
- package/dist/components/ui/sidebar/next-router.cjs +1 -119
- package/dist/components/ui/sidebar/sidebar-back-button.js +1 -8
- package/dist/components/ui/sidebar/sidebar-components.js +1 -23
- package/dist/components/ui/sidebar/sidebar-expand-button.js +1 -49
- package/dist/components/ui/sidebar/sidebar-item-button.js +1 -21
- package/dist/components/ui/sidebar/sidebar-item-collapsible.js +1 -52
- package/dist/components/ui/sidebar/sidebar-item-icon-button.js +1 -18
- package/dist/components/ui/sidebar/sidebar-item.js +1 -44
- package/dist/components/ui/sidebar/sidebar-provider.js +1 -201
- package/dist/components/ui/sidebar/sidebar-root.d.ts +3 -1
- package/dist/components/ui/sidebar/sidebar-root.d.ts.map +1 -1
- package/dist/components/ui/sidebar/sidebar-root.js +1 -130
- package/dist/components/ui/sidebar/sidebar-router.js +1 -126
- package/dist/components/ui/sidebar/sidebar-trigger.js +1 -57
- package/dist/components/ui/skeleton/index.js +1 -8
- package/dist/components/ui/stepper/index.js +1 -29
- package/dist/components/ui/switch/index.js +1 -43
- package/dist/components/ui/table/index.js +1 -56
- package/dist/components/ui/tabs/index.js +1 -55
- package/dist/components/ui/textarea/index.js +1 -8
- package/dist/components/ui/time-picker/index.js +1 -85
- package/dist/components/ui/toast/index.js +1 -3
- package/dist/components/ui/toast/toaster.js +1 -16
- package/dist/components/ui/toggle-group/index.js +1 -156
- package/dist/components/ui/tooltip/index.js +1 -81
- package/dist/domain/blotter/index.js +1 -20
- package/dist/domain/delinquency-aging/aging-buckets.js +1 -120
- package/dist/domain/delinquency-aging/index.js +1 -222
- package/dist/domain/figure/index.js +1 -19
- package/dist/domain/format.js +1 -137
- package/dist/domain/index.js +1 -43
- package/dist/domain/key-id/index.js +1 -174
- package/dist/domain/ledger-sheet/index.js +1 -28
- package/dist/domain/money-diff.js +1 -48
- package/dist/domain/money-math.js +1 -104
- package/dist/domain/money-text/index.js +1 -123
- package/dist/domain/section-label/index.js +1 -17
- package/dist/domain/status-rail/index.js +1 -76
- package/dist/domain/threshold-gauge/index.js +1 -161
- package/dist/enterprise/alert-banner/index.js +1 -24
- package/dist/enterprise/app-shell/index.js +1 -87
- package/dist/enterprise/cursor-pager/index.js +1 -26
- package/dist/enterprise/data-table/column-size.js +1 -103
- package/dist/enterprise/data-table/index.d.ts.map +1 -1
- package/dist/enterprise/data-table/index.js +1 -345
- package/dist/enterprise/date-range-picker/index.js +1 -115
- package/dist/enterprise/detail-panel/index.js +1 -20
- package/dist/enterprise/empty-state/index.d.ts +6 -1
- package/dist/enterprise/empty-state/index.d.ts.map +1 -1
- package/dist/enterprise/empty-state/index.js +1 -13
- package/dist/enterprise/index.js +1 -39
- package/dist/enterprise/number-input/index.js +1 -252
- package/dist/enterprise/search-input/index.js +1 -121
- package/dist/enterprise/stat-card/index.js +1 -75
- package/dist/enterprise/status-badge/index.js +1 -120
- package/dist/enterprise/use-is-mobile.js +1 -68
- package/dist/enterprise/virtualized-table/index.js +1 -141
- package/dist/esm/charts/chart.js +4 -190
- package/dist/esm/charts/index.js +1 -3
- package/dist/esm/components/breadcrumb/get-breadcrumb-paths.js +1 -12
- package/dist/esm/components/breadcrumb/index.js +1 -30
- package/dist/esm/components/card/account-card/index.js +1 -53
- package/dist/esm/components/card/basic-card/index.js +1 -14
- package/dist/esm/components/card/dashboard-card/index.js +1 -14
- package/dist/esm/components/card/entity-card/index.js +1 -52
- package/dist/esm/components/card/index.js +1 -4
- package/dist/esm/components/dialog/confirmation-dialog/index.d.ts.map +1 -1
- package/dist/esm/components/dialog/confirmation-dialog/index.js +1 -60
- package/dist/esm/components/dialog/confirmation-dialog/use-confirm-dialog.js +1 -49
- package/dist/esm/components/dialog/index.js +1 -3
- package/dist/esm/components/dialog/large-dialog/index.d.ts +2 -2
- package/dist/esm/components/dialog/large-dialog/index.d.ts.map +1 -1
- package/dist/esm/components/dialog/large-dialog/index.js +1 -16
- package/dist/esm/components/entity-box/index.js +1 -56
- package/dist/esm/components/entity-data-table/index.js +1 -17
- package/dist/esm/components/form/combo-box-field/index.js +1 -28
- package/dist/esm/components/form/country-field/index.js +1 -7
- package/dist/esm/components/form/date-picker-field/index.js +1 -43
- package/dist/esm/components/form/date-range-field/index.js +1 -18
- package/dist/esm/components/form/date-time-picker-field/index.js +1 -74
- package/dist/esm/components/form/file-upload-field/index.js +1 -65
- package/dist/esm/components/form/index.js +1 -15
- package/dist/esm/components/form/input-field/index.js +1 -92
- package/dist/esm/components/form/otp-field/index.js +1 -15
- package/dist/esm/components/form/pagination-limit-field/index.js +1 -6
- package/dist/esm/components/form/password-field/index.d.ts.map +1 -1
- package/dist/esm/components/form/password-field/index.js +1 -10
- package/dist/esm/components/form/radio-group-field/index.js +1 -32
- package/dist/esm/components/form/renderable-label.js +1 -54
- package/dist/esm/components/form/select-field/index.js +1 -64
- package/dist/esm/components/form/state-field/index.js +1 -35
- package/dist/esm/components/form/switch-field/index.js +1 -12
- package/dist/esm/components/form/textarea-field/index.js +1 -19
- package/dist/esm/components/page/index.js +1 -15
- package/dist/esm/components/page-header/index.js +1 -71
- package/dist/esm/components/pagination/index.js +1 -6
- package/dist/esm/components/table/id-table-cell.js +1 -105
- package/dist/esm/components/table/index.js +1 -3
- package/dist/esm/components/table/locked-table-actions.js +1 -37
- package/dist/esm/components/table/name-table-cell.js +1 -54
- package/dist/esm/components/ui/accordion/index.js +1 -24
- package/dist/esm/components/ui/alert/index.js +1 -38
- package/dist/esm/components/ui/alert-dialog/index.js +1 -39
- package/dist/esm/components/ui/autocomplete/index.js +1 -392
- package/dist/esm/components/ui/autosize-textarea/index.js +1 -51
- package/dist/esm/components/ui/avatar/index.js +1 -14
- package/dist/esm/components/ui/badge/index.js +1 -79
- package/dist/esm/components/ui/breadcrumb/index.js +1 -27
- package/dist/esm/components/ui/button/index.js +1 -60
- package/dist/esm/components/ui/calendar/index.js +1 -82
- package/dist/esm/components/ui/card/index.js +1 -24
- package/dist/esm/components/ui/checkbox/index.js +1 -9
- package/dist/esm/components/ui/collapsible/index.js +1 -13
- package/dist/esm/components/ui/command/index.js +1 -51
- package/dist/esm/components/ui/copy-field/index.js +1 -127
- package/dist/esm/components/ui/dialog/index.d.ts +4 -1
- package/dist/esm/components/ui/dialog/index.d.ts.map +1 -1
- package/dist/esm/components/ui/dialog/index.js +1 -36
- package/dist/esm/components/ui/dropdown-menu/index.js +1 -60
- package/dist/esm/components/ui/field/index.js +1 -71
- package/dist/esm/components/ui/file-upload/index.js +1 -536
- package/dist/esm/components/ui/form.js +1 -118
- package/dist/esm/components/ui/hover-card/index.js +1 -14
- package/dist/esm/components/ui/icon-button/index.js +1 -31
- package/dist/esm/components/ui/input/adornment.js +1 -17
- package/dist/esm/components/ui/input/index.js +1 -60
- package/dist/esm/components/ui/label/index.js +1 -10
- package/dist/esm/components/ui/lerian-wordmark/index.js +1 -4
- package/dist/esm/components/ui/loading-button/index.js +1 -7
- package/dist/esm/components/ui/multiple-select/index.js +1 -196
- package/dist/esm/components/ui/otp-input/index.js +1 -21
- package/dist/esm/components/ui/paper/index.js +1 -5
- package/dist/esm/components/ui/paper-collapsible/index.js +1 -19
- package/dist/esm/components/ui/popover/index.js +1 -17
- package/dist/esm/components/ui/progress/index.js +1 -31
- package/dist/esm/components/ui/qr-code/index.js +1 -24
- package/dist/esm/components/ui/radio-group/index.js +1 -12
- package/dist/esm/components/ui/scroll-area/index.js +1 -13
- package/dist/esm/components/ui/select/index.js +1 -74
- package/dist/esm/components/ui/separator/index.js +1 -8
- package/dist/esm/components/ui/sheet/index.d.ts +7 -2
- package/dist/esm/components/ui/sheet/index.d.ts.map +1 -1
- package/dist/esm/components/ui/sheet/index.js +1 -92
- package/dist/esm/components/ui/sidebar/index.js +1 -11
- package/dist/esm/components/ui/sidebar/next-router.cjs +1 -119
- package/dist/esm/components/ui/sidebar/sidebar-back-button.js +1 -5
- package/dist/esm/components/ui/sidebar/sidebar-components.js +1 -15
- package/dist/esm/components/ui/sidebar/sidebar-expand-button.js +1 -42
- package/dist/esm/components/ui/sidebar/sidebar-item-button.js +1 -18
- package/dist/esm/components/ui/sidebar/sidebar-item-collapsible.js +1 -44
- package/dist/esm/components/ui/sidebar/sidebar-item-icon-button.js +1 -14
- package/dist/esm/components/ui/sidebar/sidebar-item.js +1 -40
- package/dist/esm/components/ui/sidebar/sidebar-provider.js +1 -193
- package/dist/esm/components/ui/sidebar/sidebar-root.d.ts +3 -1
- package/dist/esm/components/ui/sidebar/sidebar-root.d.ts.map +1 -1
- package/dist/esm/components/ui/sidebar/sidebar-root.js +1 -126
- package/dist/esm/components/ui/sidebar/sidebar-router.js +1 -89
- package/dist/esm/components/ui/sidebar/sidebar-trigger.js +1 -53
- package/dist/esm/components/ui/skeleton/index.js +1 -6
- package/dist/esm/components/ui/stepper/index.js +1 -20
- package/dist/esm/components/ui/switch/index.js +1 -8
- package/dist/esm/components/ui/table/index.js +1 -44
- package/dist/esm/components/ui/tabs/index.js +1 -17
- package/dist/esm/components/ui/textarea/index.js +1 -6
- package/dist/esm/components/ui/time-picker/index.js +1 -49
- package/dist/esm/components/ui/toast/index.js +1 -2
- package/dist/esm/components/ui/toast/toaster.js +1 -13
- package/dist/esm/components/ui/toggle-group/index.js +1 -120
- package/dist/esm/components/ui/tooltip/index.js +1 -43
- package/dist/esm/domain/blotter/index.js +1 -16
- package/dist/esm/domain/delinquency-aging/aging-buckets.js +1 -115
- package/dist/esm/domain/delinquency-aging/index.js +1 -218
- package/dist/esm/domain/figure/index.js +1 -15
- package/dist/esm/domain/format.js +1 -128
- package/dist/esm/domain/index.js +1 -19
- package/dist/esm/domain/key-id/index.js +1 -169
- package/dist/esm/domain/ledger-sheet/index.js +1 -24
- package/dist/esm/domain/money-diff.js +1 -45
- package/dist/esm/domain/money-math.js +1 -98
- package/dist/esm/domain/money-text/index.js +1 -118
- package/dist/esm/domain/section-label/index.js +1 -14
- package/dist/esm/domain/status-rail/index.js +1 -71
- package/dist/esm/domain/threshold-gauge/index.js +1 -157
- package/dist/esm/enterprise/alert-banner/index.js +1 -21
- package/dist/esm/enterprise/app-shell/index.js +1 -51
- package/dist/esm/enterprise/cursor-pager/index.js +1 -23
- package/dist/esm/enterprise/data-table/column-size.js +1 -97
- package/dist/esm/enterprise/data-table/index.d.ts.map +1 -1
- package/dist/esm/enterprise/data-table/index.js +1 -342
- package/dist/esm/enterprise/date-range-picker/index.js +1 -112
- package/dist/esm/enterprise/detail-panel/index.js +1 -17
- package/dist/esm/enterprise/empty-state/index.d.ts +6 -1
- package/dist/esm/enterprise/empty-state/index.d.ts.map +1 -1
- package/dist/esm/enterprise/empty-state/index.js +1 -10
- package/dist/esm/enterprise/index.js +1 -22
- package/dist/esm/enterprise/number-input/index.js +1 -214
- package/dist/esm/enterprise/search-input/index.js +1 -85
- package/dist/esm/enterprise/stat-card/index.js +1 -72
- package/dist/esm/enterprise/status-badge/index.js +1 -116
- package/dist/esm/enterprise/use-is-mobile.js +1 -31
- package/dist/esm/enterprise/virtualized-table/index.js +1 -138
- package/dist/esm/hooks/use-click-away.js +1 -22
- package/dist/esm/hooks/use-pagination.js +1 -38
- package/dist/esm/hooks/use-stepper.js +1 -22
- package/dist/esm/hooks/use-time.js +1 -13
- package/dist/esm/hooks/use-toast.js +1 -107
- package/dist/esm/index.js +1 -77
- package/dist/esm/lib/storage/get-storage-object.js +1 -17
- package/dist/esm/lib/storage/get-storage.js +1 -14
- package/dist/esm/lib/storage/index.js +1 -2
- package/dist/esm/lib/typography.js +1 -31
- package/dist/esm/lib/utils.js +1 -41
- package/dist/esm/theme/get-theme-script.js +1 -41
- package/dist/esm/theme/index.js +1 -4
- package/dist/esm/theme/mode-toggle.js +1 -45
- package/dist/esm/theme/theme-provider.js +1 -122
- package/dist/esm/toast/helpers.js +1 -36
- package/dist/esm/toast/index.js +1 -4
- package/dist/esm/types/metadata-type.js +1 -1
- package/dist/esm/utils/countries.cjs +1 -17
- package/dist/esm/utils/country-utils.js +1 -18
- package/dist/esm/utils/currency-codes.js +1 -181
- package/dist/globals.css +1 -0
- package/dist/hooks/use-click-away.js +1 -29
- package/dist/hooks/use-pagination.js +1 -41
- package/dist/hooks/use-stepper.js +1 -26
- package/dist/hooks/use-time.js +1 -20
- package/dist/hooks/use-toast.js +1 -111
- package/dist/index.js +1 -96
- package/dist/lib/storage/get-storage-object.js +1 -23
- package/dist/lib/storage/get-storage.js +1 -17
- package/dist/lib/storage/index.js +1 -18
- package/dist/lib/typography.js +1 -34
- package/dist/lib/utils.js +1 -45
- package/dist/theme/get-theme-script.js +1 -44
- package/dist/theme/index.js +1 -11
- package/dist/theme/mode-toggle.js +1 -48
- package/dist/theme/theme-provider.js +1 -160
- package/dist/toast/helpers.js +1 -41
- package/dist/toast/index.js +1 -10
- package/dist/tokens.css +7 -4
- package/dist/types/metadata-type.js +1 -2
- package/dist/utils/countries.cjs +1 -17
- package/dist/utils/country-utils.js +1 -25
- package/dist/utils/currency-codes.js +1 -184
- package/package.json +4 -3
|
@@ -1,98 +1 @@
|
|
|
1
|
-
|
|
2
|
-
* money-math — exact integer-minor-unit arithmetic for the double-entry third rail.
|
|
3
|
-
*
|
|
4
|
-
* INTERNAL to the domain layer: nothing here is re-exported from
|
|
5
|
-
* `src/domain/index.ts`, so `toMinor`/`Amount`/`sumMinor` are NOT part of
|
|
6
|
-
* `@lerianstudio/sindarian-ui`'s public API. The consumers that need exact money
|
|
7
|
-
* arithmetic reach it through the components that own it (`moneyDiff`,
|
|
8
|
-
* `MoneyText`, `DelinquencyAging`).
|
|
9
|
-
*
|
|
10
|
-
* Amounts parse to integer minor units (BigInt) at a fixed scale; sums never
|
|
11
|
-
* round-trip through IEEE-754. Pass amounts as canonical decimal STRINGS for
|
|
12
|
-
* lossless math — numbers are converted via toFixed, which is float-bounded
|
|
13
|
-
* (fine for display-scale magnitudes, exact for strings).
|
|
14
|
-
*/
|
|
15
|
-
import { isValidDigitCount } from './format.js';
|
|
16
|
-
// Round half-away-from-zero on the first dropped fraction digit.
|
|
17
|
-
function roundsUp(fraction, scale) {
|
|
18
|
-
return fraction.length > scale && fraction.charCodeAt(scale) - 48 >= 5;
|
|
19
|
-
}
|
|
20
|
-
/** Normalize to a canonical signed decimal: trim, unicode-minus, accounting parens. */
|
|
21
|
-
function canonical(raw) {
|
|
22
|
-
let s = raw.trim().replace(/−/g, '-');
|
|
23
|
-
if (/^\(.*\)$/.test(s))
|
|
24
|
-
s = '-' + s.slice(1, -1).trim(); // (1.23) -> -1.23
|
|
25
|
-
return s;
|
|
26
|
-
}
|
|
27
|
-
/**
|
|
28
|
-
* Amount -> integer minor units at `scale`, or null if unparseable.
|
|
29
|
-
*
|
|
30
|
-
* `scale` is validated before it reaches `toFixed`/`repeat`: an out-of-range
|
|
31
|
-
* scale (a caller passing `minorDigits: -1`) would throw a RangeError mid-render
|
|
32
|
-
* rather than degrade. It is the single choke point every money path in this
|
|
33
|
-
* layer routes through (`sumMinor`, `moneyDiff`, `delinquencyRate`,
|
|
34
|
-
* `sumBucketTotals`), so guarding it here guards all of them.
|
|
35
|
-
*/
|
|
36
|
-
export function toMinor(value, scale) {
|
|
37
|
-
if (!isValidDigitCount(scale))
|
|
38
|
-
return null;
|
|
39
|
-
if (value === null || value === undefined || value === '')
|
|
40
|
-
return null;
|
|
41
|
-
let s = typeof value === 'number'
|
|
42
|
-
? Number.isFinite(value)
|
|
43
|
-
? value.toFixed(scale)
|
|
44
|
-
: ''
|
|
45
|
-
: canonical(value);
|
|
46
|
-
if (s === '' || s === '-' || s === '.' || !/^-?\d*(\.\d*)?$/.test(s))
|
|
47
|
-
return null;
|
|
48
|
-
const neg = s.startsWith('-');
|
|
49
|
-
if (neg)
|
|
50
|
-
s = s.slice(1);
|
|
51
|
-
const [intPart = '', fracPart = ''] = s.split('.');
|
|
52
|
-
// DELIBERATE DIVERGENCE from sindarian-x@0.15.0: the legacy regex admits a
|
|
53
|
-
// digitless token ('-.', and '.' once the sign is stripped) and coerced it to
|
|
54
|
-
// an exact 0n. A fabricated exact zero is the worst possible answer in
|
|
55
|
-
// reconciliation — it reads as a perfect tie and hides the bad input — so a
|
|
56
|
-
// digitless amount is now indeterminate (null), like every other garbage
|
|
57
|
-
// token. This mirrors the guard the legacy `rateFraction` already had.
|
|
58
|
-
if (intPart === '' && fracPart === '')
|
|
59
|
-
return null;
|
|
60
|
-
const frac = (fracPart + '0'.repeat(scale)).slice(0, scale);
|
|
61
|
-
let minor = BigInt((intPart || '0') + frac);
|
|
62
|
-
if (roundsUp(fracPart, scale))
|
|
63
|
-
minor += 1n;
|
|
64
|
-
return neg ? -minor : minor;
|
|
65
|
-
}
|
|
66
|
-
/** Sum amounts to minor units; null if ANY operand is unparseable (indeterminate). */
|
|
67
|
-
export function sumMinor(values, scale) {
|
|
68
|
-
let total = 0n;
|
|
69
|
-
for (const v of values) {
|
|
70
|
-
const m = toMinor(v, scale);
|
|
71
|
-
if (m === null)
|
|
72
|
-
return null;
|
|
73
|
-
total += m;
|
|
74
|
-
}
|
|
75
|
-
return total;
|
|
76
|
-
}
|
|
77
|
-
/** Currency's minor-unit digit count via CLDR (BRL/USD->2, JPY->0, BHD->3), or fallback. */
|
|
78
|
-
export function minorDigitsOf(currency, fallback = 2) {
|
|
79
|
-
try {
|
|
80
|
-
const opts = new Intl.NumberFormat('en', {
|
|
81
|
-
style: 'currency',
|
|
82
|
-
currency,
|
|
83
|
-
currencyDisplay: 'code'
|
|
84
|
-
}).resolvedOptions();
|
|
85
|
-
return opts.maximumFractionDigits ?? fallback;
|
|
86
|
-
}
|
|
87
|
-
catch {
|
|
88
|
-
return fallback;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
/** Minor units -> canonical decimal string (lossless), for handing to MoneyText. */
|
|
92
|
-
export function minorToDecimal(minor, scale) {
|
|
93
|
-
const neg = minor < 0n;
|
|
94
|
-
const abs = (neg ? -minor : minor).toString().padStart(scale + 1, '0');
|
|
95
|
-
const intPart = abs.slice(0, abs.length - scale) || '0';
|
|
96
|
-
const fracPart = scale > 0 ? '.' + abs.slice(abs.length - scale) : '';
|
|
97
|
-
return (neg ? '-' : '') + intPart + fracPart;
|
|
98
|
-
}
|
|
1
|
+
import{isValidDigitCount as s}from"./format.js";function f(n,t){return n.length>t&&n.charCodeAt(t)-48>=5}function l(n){let t=n.trim().replace(/−/g,"-");if(/^\(.*\)$/.test(t))t="-"+t.slice(1,-1).trim();return t}export function toMinor(n,t){if(!s(t))return null;if(n===null||n===void 0||n==="")return null;let r=typeof n==="number"?Number.isFinite(n)?n.toFixed(t):"":l(n);if(r===""||r==="-"||r==="."||!/^-?\d*(\.\d*)?$/.test(r))return null;const i=r.startsWith("-");if(i)r=r.slice(1);const[o="",e=""]=r.split(".");if(o===""&&e==="")return null;const c=(e+"0".repeat(t)).slice(0,t);let u=BigInt((o||"0")+c);if(f(e,t))u+=1n;return i?-u:u}export function sumMinor(n,t){let r=0n;for(const i of n){const o=toMinor(i,t);if(o===null)return null;r+=o}return r}export function minorDigitsOf(n,t=2){try{const r=new Intl.NumberFormat("en",{style:"currency",currency:n,currencyDisplay:"code"}).resolvedOptions();return r.maximumFractionDigits??t}catch{return t}}export function minorToDecimal(n,t){const r=n<0n;const i=(r?-n:n).toString().padStart(t+1,"0");const o=i.slice(0,i.length-t)||"0";const e=t>0?"."+i.slice(i.length-t):"";return(r?"-":"")+o+e}
|
|
@@ -1,118 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
/**
|
|
3
|
-
* MoneyText renders a monetary amount with optional ISO currency code,
|
|
4
|
-
* tabular figures so columns line up. The amount is formatted losslessly: when
|
|
5
|
-
* passed as a string, `Intl.NumberFormat.format()` (ES2020+) parses it as an
|
|
6
|
-
* arbitrary-precision value and rounds to `fractionDigits` without ever
|
|
7
|
-
* round-tripping through an IEEE-754 double — so 16+ significant-digit tokens
|
|
8
|
-
* keep every digit. Pass the raw decimal string (not a pre-coerced Number) to
|
|
9
|
-
* preserve that guarantee.
|
|
10
|
-
*
|
|
11
|
-
* The currency code is shown as a suffix rather than a symbol so mixed-currency
|
|
12
|
-
* ledgers stay unambiguous. Negative amounts get the error sign color;
|
|
13
|
-
* null/undefined/empty render the muted no-value placeholder.
|
|
14
|
-
*
|
|
15
|
-
* SIGN COLOR IS NOT THE CREDIT ROLE. The sign ink marks a negative/alarm
|
|
16
|
-
* amount; `--credit` is the accounting reading of a credit amount. They are two
|
|
17
|
-
* roles, and a consumer layers the credit role on top (matcher's fork passes
|
|
18
|
-
* `signColor={false}` with `tone="credit"` precisely so the two never fight).
|
|
19
|
-
* Collapsing the sign color into `text-credit` would erase that distinction.
|
|
20
|
-
*
|
|
21
|
-
* THE SIGN INK IS THE `system-error` H1A INK, not `--destructive`. A signed
|
|
22
|
-
* amount is TEXT, so it answers to WCAG AA on whatever surface holds it, and
|
|
23
|
-
* `--destructive` is a FILL family (buttons, badges, tinted chips) whose dark
|
|
24
|
-
* value reads 3.80:1 as ink on `--card`, an axe finding on a live console.
|
|
25
|
-
* `--system-error-h1a` is the error INK of the same family and reads 8.28:1
|
|
26
|
-
* light / 7.12:1 dark on that surface. `__tests__/tokens-contract.test.ts`
|
|
27
|
-
* re-measures whichever token this line applies, so the pairing is measured
|
|
28
|
-
* rather than trusted.
|
|
29
|
-
*/
|
|
30
|
-
import { cn } from '../../lib/utils.js';
|
|
31
|
-
import { NO_VALUE, isValidDigitCount, safeLocale } from '../format.js';
|
|
32
|
-
/**
|
|
33
|
-
* Normalize a raw amount into a string the Intl formatter accepts while
|
|
34
|
-
* preserving precision: trim, convert the unicode minus U+2212 to ASCII '-',
|
|
35
|
-
* and rewrite accounting parens `(123.45)` → `-123.45`. Returns null when the
|
|
36
|
-
* input is empty after trimming (→ NO_VALUE upstream).
|
|
37
|
-
*
|
|
38
|
-
* Internal: exported for its unit tests, never from `src/domain/index.ts`.
|
|
39
|
-
*/
|
|
40
|
-
export function normalizeAmount(amount) {
|
|
41
|
-
let s = String(amount).trim();
|
|
42
|
-
if (s === '')
|
|
43
|
-
return null;
|
|
44
|
-
s = s.replace(/−/g, '-');
|
|
45
|
-
const parens = s.match(/^\((.*)\)$/);
|
|
46
|
-
if (parens)
|
|
47
|
-
s = `-${parens[1].trim()}`;
|
|
48
|
-
return s;
|
|
49
|
-
}
|
|
50
|
-
/**
|
|
51
|
-
* Pure money formatting: normalize the amount, format it via Intl (preserving
|
|
52
|
-
* precision through the ES2020 string overload), and derive sign + finiteness
|
|
53
|
-
* from the formatted parts — so color and glyph can never disagree. Returns
|
|
54
|
-
* null for empty, non-finite, or unparseable input (→ NO_VALUE upstream).
|
|
55
|
-
*
|
|
56
|
-
* Internal: exported for its unit tests, never from `src/domain/index.ts`.
|
|
57
|
-
*/
|
|
58
|
-
export function formatMoneyParts(amount, fractionDigits, locale) {
|
|
59
|
-
// Reject non-finite numeric inputs (NaN / ±Infinity) before they reach Intl,
|
|
60
|
-
// which would otherwise render literal "NaN" / "∞".
|
|
61
|
-
if (typeof amount === 'number' && !Number.isFinite(amount))
|
|
62
|
-
return null;
|
|
63
|
-
// A caller-supplied digit count reaches Intl directly; an out-of-range one
|
|
64
|
-
// would throw a RangeError and blank the amount instead of degrading to
|
|
65
|
-
// NO_VALUE.
|
|
66
|
-
if (!isValidDigitCount(fractionDigits))
|
|
67
|
-
return null;
|
|
68
|
-
const normalized = normalizeAmount(amount);
|
|
69
|
-
if (normalized === null)
|
|
70
|
-
return null;
|
|
71
|
-
// DELIBERATE DIVERGENCE from sindarian-x@0.15.0: an exact zero written with a
|
|
72
|
-
// sign ('-0', '-0.00', '(0)') formatted as "-0.00" and reported negative:true,
|
|
73
|
-
// which painted a zero balance in the destructive sign color. Zero is neither
|
|
74
|
-
// positive nor negative — a signed-zero ledger cell is a rendering artifact of
|
|
75
|
-
// the input notation, never a fact about the money. The sign is dropped for an
|
|
76
|
-
// EXACT zero only; '-0.004' still rounds to "-0.00" and stays negative,
|
|
77
|
-
// because that one really is below zero.
|
|
78
|
-
const exactZero = /\d/.test(normalized) && /^-?0*(\.0*)?$/.test(normalized);
|
|
79
|
-
const forFormat = exactZero ? normalized.replace(/^-/, '') : normalized;
|
|
80
|
-
// A malformed tag would throw a RangeError out of this constructor and take
|
|
81
|
-
// the whole amount down; degrade to the runtime locale instead.
|
|
82
|
-
const formatter = new Intl.NumberFormat(safeLocale(locale), {
|
|
83
|
-
minimumFractionDigits: fractionDigits,
|
|
84
|
-
maximumFractionDigits: fractionDigits,
|
|
85
|
-
signDisplay: 'auto'
|
|
86
|
-
});
|
|
87
|
-
// The bundled lib types only model a `number | bigint` overload; the ES2020
|
|
88
|
-
// string overload (which preserves precision) is unmodelled, so the
|
|
89
|
-
// normalized amount is cast at the boundary.
|
|
90
|
-
//
|
|
91
|
-
// RUNTIME FLOOR: this relies on a V8 with exact decimal-STRING Intl support
|
|
92
|
-
// (Node >= 20, evergreen browsers) — the whole point is that a 20-digit amount
|
|
93
|
-
// never round-trips through an IEEE-754 double. There is deliberately NO
|
|
94
|
-
// Number-coercion fallback for older runtimes: that fallback would silently
|
|
95
|
-
// reintroduce the precision loss this function exists to prevent.
|
|
96
|
-
const parts = formatter.formatToParts(forFormat);
|
|
97
|
-
// Intl emits a literal "nan"/"∞" part for non-finite values; treat those as
|
|
98
|
-
// no-value rather than rendering them.
|
|
99
|
-
if (parts.some((p) => p.type === 'nan' || p.type === 'infinity'))
|
|
100
|
-
return null;
|
|
101
|
-
return {
|
|
102
|
-
formatted: parts.map((p) => p.value).join(''),
|
|
103
|
-
negative: parts.some((p) => p.type === 'minusSign')
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
export function MoneyText({ amount, currency, fractionDigits = 2, hideCurrency = false, signColor = true, locale, className }) {
|
|
107
|
-
const noValue = (_jsx("span", { className: cn('text-muted-foreground tabular-nums', className), children: NO_VALUE }));
|
|
108
|
-
if (amount === null || amount === undefined || amount === '')
|
|
109
|
-
return noValue;
|
|
110
|
-
// Sign and finiteness are derived from the formatted parts, not a raw string
|
|
111
|
-
// prefix — so color and glyph can never disagree, and "-0" / "(0)" are
|
|
112
|
-
// treated as the non-negative zero Intl renders.
|
|
113
|
-
const result = formatMoneyParts(amount, fractionDigits, locale);
|
|
114
|
-
if (result === null)
|
|
115
|
-
return noValue;
|
|
116
|
-
const { formatted, negative } = result;
|
|
117
|
-
return (_jsxs("span", { className: cn('tabular-nums', signColor && negative && 'text-system-error-h1a', className), children: [formatted, !hideCurrency && currency ? (_jsx("span", { className: "text-muted-foreground ml-1 text-xs", children: currency })) : null] }));
|
|
118
|
-
}
|
|
1
|
+
import{jsx as u,jsxs as p}from"react/jsx-runtime";import{cn as m}from"../../lib/utils.js";import{NO_VALUE as x,isValidDigitCount as d,safeLocale as g}from"../format.js";export function normalizeAmount(e){let t=String(e).trim();if(t==="")return null;t=t.replace(/−/g,"-");const s=t.match(/^\((.*)\)$/);if(s)t=`-${s[1].trim()}`;return t}export function formatMoneyParts(e,t,s){if(typeof e==="number"&&!Number.isFinite(e))return null;if(!d(t))return null;const r=normalizeAmount(e);if(r===null)return null;const a=/\d/.test(r)&&/^-?0*(\.0*)?$/.test(r);const l=a?r.replace(/^-/,""):r;const i=new Intl.NumberFormat(g(s),{minimumFractionDigits:t,maximumFractionDigits:t,signDisplay:"auto"});const o=i.formatToParts(l);if(o.some(n=>n.type==="nan"||n.type==="infinity"))return null;return{formatted:o.map(n=>n.value).join(""),negative:o.some(n=>n.type==="minusSign")}}export function MoneyText({amount:e,currency:t,fractionDigits:s=2,hideCurrency:r=false,signColor:a=true,locale:l,className:i}){const o=u("span",{className:m("text-muted-foreground tabular-nums",i),children:x});if(e===null||e===void 0||e==="")return o;const n=formatMoneyParts(e,s,l);if(n===null)return o;const{formatted:f,negative:c}=n;return p("span",{className:m("tabular-nums",a&&c&&"text-system-error-h1a",i),children:[f,!r&&t?u("span",{className:"text-muted-foreground ml-1 text-xs",children:t}):null]})}
|
|
@@ -1,14 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { SECTION_LABEL_CLASS } from '../../lib/typography.js';
|
|
3
|
-
import { cn } from '../../lib/utils.js';
|
|
4
|
-
/** The entry-section head treatment: a hairline double rule under the label.
|
|
5
|
-
* `border-border`, NOT the register head's ink — this is the quieter of the two
|
|
6
|
-
* head rules. Pinned here because ~20 call sites were restating it. */
|
|
7
|
-
const RULED_CLASS = 'border-b-[3px] border-double border-border pb-2';
|
|
8
|
-
export function SectionLabel({ children, className, as: Tag = 'h2', ruled = false, ...rest }) {
|
|
9
|
-
// The remaining element props reach the rendered tag, so a panel can point
|
|
10
|
-
// `aria-labelledby` at this heading via `id`, and tests can hang a `data-*`
|
|
11
|
-
// hook off it. Without the spread there was no way to reference the heading
|
|
12
|
-
// from its own container.
|
|
13
|
-
return (_jsx(Tag, { ...rest, "data-variant": ruled ? 'ruled' : undefined, className: cn(SECTION_LABEL_CLASS, ruled && RULED_CLASS, className), children: children }));
|
|
14
|
-
}
|
|
1
|
+
import{jsx as t}from"react/jsx-runtime";import{SECTION_LABEL_CLASS as d}from"../../lib/typography.js";import{cn as b}from"../../lib/utils.js";const i="border-b-[3px] border-double border-border pb-2";export function SectionLabel({children:o,className:e,as:a="h2",ruled:r=false,...n}){return t(a,{...n,"data-variant":r?"ruled":void 0,className:b(d,r&&i,e),children:o})}
|
|
@@ -1,71 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
/**
|
|
3
|
-
* StatusRail — Row 0 of every console screen. A thin, full-width recessed strip
|
|
4
|
-
* that reads "the terminal is live": a dot-separated mono tape on the recessed
|
|
5
|
-
* page surface (`bg-body-surface`), with optional right-pinned count chips.
|
|
6
|
-
*
|
|
7
|
-
* <StatusRail
|
|
8
|
-
* lead={contextName}
|
|
9
|
-
* items={[
|
|
10
|
-
* { value: '90d' },
|
|
11
|
-
* { label: 'Updated', value: '12:04 UTC' },
|
|
12
|
-
* { value: <><LivePulse /> Live</> }
|
|
13
|
-
* ]}
|
|
14
|
-
* chips={[
|
|
15
|
-
* { label: 'Open', value: '12' },
|
|
16
|
-
* { label: 'Pending overdue', value: '3', alarm: true }
|
|
17
|
-
* ]}
|
|
18
|
-
* />
|
|
19
|
-
*
|
|
20
|
-
* The first token (`lead`) is `text-foreground font-medium`. Tape items are
|
|
21
|
-
* dot-separated; an item with a `label` renders the label inline before its
|
|
22
|
-
* value, in the rail's own `font-medium`. Chips pin right via `ml-auto`;
|
|
23
|
-
* `alarm` escalates a chip to destructive.
|
|
24
|
-
*
|
|
25
|
-
* Labels carry weight only: the rail root already supplies size, family and
|
|
26
|
-
* ink (`font-mono text-xs text-muted-foreground`), so re-stating
|
|
27
|
-
* LABEL_VOICE_CLASS here would import a `text-sm` that fights the tape.
|
|
28
|
-
*/
|
|
29
|
-
import { TriangleAlert } from 'lucide-react';
|
|
30
|
-
import { cn } from '../../lib/utils.js';
|
|
31
|
-
export function StatusRail({ lead, items = [], chips = [], className }) {
|
|
32
|
-
// ONE test, used by both the lead span and the first item's separator. Two
|
|
33
|
-
// separate tests (`lead !== undefined && lead !== null` vs `lead != null`)
|
|
34
|
-
// agreed on null/undefined and diverged on a ReactNode that RENDERS NOTHING,
|
|
35
|
-
// which produced an empty lead span while the first item still got its
|
|
36
|
-
// leading dot — the rail opened with a stray `·`. React renders BOTH booleans
|
|
37
|
-
// as nothing, so `true` is as empty as `false`; the typeof check covers the
|
|
38
|
-
// pair rather than naming one of them.
|
|
39
|
-
const hasLead = lead != null && lead !== '' && typeof lead !== 'boolean';
|
|
40
|
-
return (_jsxs("div", { className: cn(
|
|
41
|
-
// Recessed instrument strip: the page surface inside a hairline border
|
|
42
|
-
// whose top edge runs one step darker — light-from-above recession
|
|
43
|
-
// without any inner shadow.
|
|
44
|
-
'border-border border-t-foreground/10 bg-body-surface text-muted-foreground flex flex-wrap items-center gap-x-3 gap-y-1.5 rounded-lg border px-4 py-2 font-mono text-xs tabular-nums', className), children: [hasLead ? (_jsx("span", { className: "text-foreground font-medium", children: lead })) : null, items.map((item, i) => (_jsx(RailFragment, { hasLead: hasLead || i > 0, children: item.label != null ? (_jsxs("span", { className: "flex items-center gap-1.5", children: [_jsx("span", { className: "font-medium", children: item.label }), item.value] })) : (_jsx("span", { className: "flex items-center gap-1.5", children: item.value })) }, item.id ?? i))), chips.length ? (_jsx("span", { className: "ml-auto flex flex-wrap items-center gap-x-3 gap-y-1.5", children: chips.map((chip, i) => (_jsxs("span", { className: "inline-flex items-center gap-1.5", children: [_jsx("span", { className: "font-medium", children: chip.label }), _jsxs("span", { className: cn(
|
|
45
|
-
// Tiny mono token: raised a hair off the sunken tape by the
|
|
46
|
-
// card surface + whisper shadow.
|
|
47
|
-
'inline-flex items-center gap-1 rounded-sm border px-1.5 py-0.5 shadow-sm', chip.alarm
|
|
48
|
-
? 'border-destructive/40 bg-destructive/10 text-system-error-h1a'
|
|
49
|
-
: 'border-border bg-card text-foreground'), children: [chip.alarm ? (_jsxs(_Fragment, { children: [_jsx(TriangleAlert, { "aria-hidden": true, className: "size-3" }), _jsx("span", { className: "sr-only", children: "(alarme)" })] })) : null, chip.value] })] }, chip.id ?? i))) })) : null] }));
|
|
50
|
-
}
|
|
51
|
-
/** A tape item preceded by a separator dot when it is not the first token. */
|
|
52
|
-
function RailFragment({ children, hasLead }) {
|
|
53
|
-
return (_jsxs(_Fragment, { children: [hasLead ? _jsx(Dot, {}) : null, children] }));
|
|
54
|
-
}
|
|
55
|
-
/** Decorative dot separator — muted, so the tape reads as punctuated, not broken. */
|
|
56
|
-
export function Dot() {
|
|
57
|
-
return (_jsx("span", { "aria-hidden": true, className: "text-muted-foreground/50", children: "\u00B7" }));
|
|
58
|
-
}
|
|
59
|
-
/**
|
|
60
|
-
* The live auto-refresh pulse: a success dot with a reduced-motion-gated ping
|
|
61
|
-
* ring. Place it inside a tape item value, e.g. `<><LivePulse /> Live</>`.
|
|
62
|
-
*
|
|
63
|
-
* The ring rides Tailwind's built-in `ping` keyframe under the `motion-safe:`
|
|
64
|
-
* variant (`@media (prefers-reduced-motion: no-preference)`), so a
|
|
65
|
-
* reduced-motion reader sees only the static dot — same gating as the
|
|
66
|
-
* hand-rolled `status-rail-pulse` keyframe it replaces, with no stylesheet of
|
|
67
|
-
* its own.
|
|
68
|
-
*/
|
|
69
|
-
export function LivePulse() {
|
|
70
|
-
return (_jsxs("span", { className: "relative flex size-2", "aria-hidden": true, children: [_jsx("span", { className: "bg-system-success/60 absolute inline-flex size-2 rounded-full motion-safe:animate-ping" }), _jsx("span", { className: "bg-system-success relative inline-flex size-2 rounded-full" })] }));
|
|
71
|
-
}
|
|
1
|
+
import{jsx as e,jsxs as r,Fragment as o}from"react/jsx-runtime";import{TriangleAlert as u}from"lucide-react";import{cn as i}from"../../lib/utils.js";export function StatusRail({lead:n,items:l=[],chips:t=[],className:m}){const d=n!=null&&n!==""&&typeof n!=="boolean";return r("div",{className:i("border-border border-t-foreground/10 bg-body-surface text-muted-foreground flex flex-wrap items-center gap-x-3 gap-y-1.5 rounded-lg border px-4 py-2 font-mono text-xs tabular-nums",m),children:[d?e("span",{className:"text-foreground font-medium",children:n}):null,l.map((a,s)=>e(c,{hasLead:d||s>0,children:a.label!=null?r("span",{className:"flex items-center gap-1.5",children:[e("span",{className:"font-medium",children:a.label}),a.value]}):e("span",{className:"flex items-center gap-1.5",children:a.value})},a.id??s)),t.length?e("span",{className:"ml-auto flex flex-wrap items-center gap-x-3 gap-y-1.5",children:t.map((a,s)=>r("span",{className:"inline-flex items-center gap-1.5",children:[e("span",{className:"font-medium",children:a.label}),r("span",{className:i("inline-flex items-center gap-1 rounded-sm border px-1.5 py-0.5 shadow-sm",a.alarm?"border-destructive/40 bg-destructive/10 text-system-error-h1a":"border-border bg-card text-foreground"),children:[a.alarm?r(o,{children:[e(u,{"aria-hidden":true,className:"size-3"}),e("span",{className:"sr-only",children:"(alarme)"})]}):null,a.value]})]},a.id??s))}):null]})}function c({children:n,hasLead:l}){return r(o,{children:[l?e(Dot,{}):null,n]})}export function Dot(){return e("span",{"aria-hidden":true,className:"text-muted-foreground/50",children:"\xB7"})}export function LivePulse(){return r("span",{className:"relative flex size-2","aria-hidden":true,children:[e("span",{className:"bg-system-success/60 absolute inline-flex size-2 rounded-full motion-safe:animate-ping"}),e("span",{className:"bg-system-success relative inline-flex size-2 rounded-full"})]})}
|
|
@@ -1,157 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
/**
|
|
3
|
-
* ThresholdGauge — where a value sits across ordered, NAMED bands.
|
|
4
|
-
*
|
|
5
|
-
* A metric is rarely interesting in isolation; what matters is which band it has
|
|
6
|
-
* crossed. A credit utilization of 95% is a breach; a liquidity coverage ratio
|
|
7
|
-
* of 95% may be perfectly calm. So a gauge that paints a smooth red→green
|
|
8
|
-
* gradient is lying by implication — it suggests a continuum where the business
|
|
9
|
-
* has hard thresholds. This atom STATES the thresholds: it resolves the current
|
|
10
|
-
* band (low / warn / breach), names it with a glyph + an sr-only word + a tint,
|
|
11
|
-
* and marks the band edges on the bar so the reader sees the line the value
|
|
12
|
-
* crossed, not a vibe.
|
|
13
|
-
*
|
|
14
|
-
* <ThresholdGauge value={0.92} max={1} warn={0.8} breach={0.9} format="percent"
|
|
15
|
-
* direction="higher-is-worse" label="Utilização do limite" /> // breach
|
|
16
|
-
* <ThresholdGauge value={1.18} max={2} warn={1} breach={0.9} format="ratio"
|
|
17
|
-
* direction="lower-is-worse" label="Índice de cobertura" /> // low (calm)
|
|
18
|
-
*
|
|
19
|
-
* DIRECTION is mandatory because the same numbers mean opposite things:
|
|
20
|
-
* - higher-is-worse: the value climbs INTO danger (warn < breach). Utilization,
|
|
21
|
-
* concentration, error rate.
|
|
22
|
-
* - lower-is-worse: the value FALLS into danger (breach < warn). Coverage,
|
|
23
|
-
* liquidity, capital adequacy.
|
|
24
|
-
*
|
|
25
|
-
* INVARIANT (strict edges by DEFAULT): a band escalation requires the value to
|
|
26
|
-
* be STRICTLY beyond the edge. A value sitting exactly ON a threshold belongs to
|
|
27
|
-
* the calmer band, not the louder one — `gaugeBand` encodes this so an on-edge
|
|
28
|
-
* utilization of exactly the breach line still reads as warn, never a surprise
|
|
29
|
-
* breach. A consumer whose own enforcement is INCLUSIVE (a quota blocking at
|
|
30
|
-
* `used >= limit`) opts out with `edges="inclusive"`, which escalates ON the
|
|
31
|
-
* edge in both directions.
|
|
32
|
-
*
|
|
33
|
-
* The current band is the load-bearing signal and it never rides on color alone
|
|
34
|
-
* (WCAG 1.4.1): a glyph leads, an sr-only word names it, the tint only
|
|
35
|
-
* reinforces. The numeric position reads via a mono `Figure`, never inferred
|
|
36
|
-
* from the bar geometry — and a consumer forced to clamp `value` to keep
|
|
37
|
-
* `aria-valuenow` in range can still print the true figure via `displayValue`.
|
|
38
|
-
*
|
|
39
|
-
* Pure display: server-safe, no directive.
|
|
40
|
-
*/
|
|
41
|
-
import { OctagonX, ShieldCheck, TriangleAlert } from 'lucide-react';
|
|
42
|
-
import { LABEL_VOICE_CLASS } from '../../lib/typography.js';
|
|
43
|
-
import { cn } from '../../lib/utils.js';
|
|
44
|
-
import { Figure } from '../figure/index.js';
|
|
45
|
-
import { formatCount, formatPercent, NO_VALUE } from '../format.js';
|
|
46
|
-
import { MoneyText } from '../money-text/index.js';
|
|
47
|
-
/** Band → glyph + tint + accessible word. The glyph and sr-only word are the
|
|
48
|
-
* load-bearing cue; the tint only reinforces, so the band survives grayscale
|
|
49
|
-
* and color-vision deficiency. The words are pt-BR defaults — a consumer in
|
|
50
|
-
* another locale overrides them via the `bandLabels` prop. */
|
|
51
|
-
const BAND = {
|
|
52
|
-
low: {
|
|
53
|
-
Icon: ShieldCheck,
|
|
54
|
-
tint: 'text-system-success-h1a',
|
|
55
|
-
word: 'Dentro do limite'
|
|
56
|
-
},
|
|
57
|
-
warn: {
|
|
58
|
-
Icon: TriangleAlert,
|
|
59
|
-
tint: 'text-system-alert-h1a',
|
|
60
|
-
word: 'Próximo do limite'
|
|
61
|
-
},
|
|
62
|
-
breach: {
|
|
63
|
-
Icon: OctagonX,
|
|
64
|
-
tint: 'text-system-error-h1a',
|
|
65
|
-
word: 'Limite ultrapassado'
|
|
66
|
-
}
|
|
67
|
-
};
|
|
68
|
-
/** Track fill tint per band, and the reason it is a SEPARATE map from the
|
|
69
|
-
* tint above: these are the fill tokens, and a fill answers to no text floor.
|
|
70
|
-
* The tint above is painted on the band glyph AND on the `Figure`, so it has
|
|
71
|
-
* to be the ink family. Light `--system-alert` is #EAB308, about 1.9:1 on
|
|
72
|
-
* white, and `--system-success` about 3.3:1 — both were readable only as a
|
|
73
|
-
* bar wash, never as the number the reader is here to read. */
|
|
74
|
-
const BAND_FILL = {
|
|
75
|
-
low: 'bg-system-success',
|
|
76
|
-
warn: 'bg-system-alert',
|
|
77
|
-
breach: 'bg-destructive'
|
|
78
|
-
};
|
|
79
|
-
/**
|
|
80
|
-
* Resolve which band a value occupies. Pure — the testable heart of the atom.
|
|
81
|
-
*
|
|
82
|
-
* STRICT edges by default: a value exactly ON a threshold stays in the CALMER
|
|
83
|
-
* band; escalation needs the value strictly past.
|
|
84
|
-
*
|
|
85
|
-
* - higher-is-worse: `value > breach` → breach; `value > warn` → warn; else low.
|
|
86
|
-
* - lower-is-worse: `value < breach` → breach; `value < warn` → warn; else low.
|
|
87
|
-
*
|
|
88
|
-
* `thresholds.edges = 'inclusive'` opts into `>=` / `<=` instead, for a consumer
|
|
89
|
-
* whose enforcement is inclusive. Omitted or 'strict' → the behaviour above.
|
|
90
|
-
*
|
|
91
|
-
* Non-finite input degrades to 'low' rather than throwing — a missing reading is
|
|
92
|
-
* not, by itself, an alarm.
|
|
93
|
-
*/
|
|
94
|
-
export function gaugeBand(value, thresholds, direction) {
|
|
95
|
-
if (!Number.isFinite(value))
|
|
96
|
-
return 'low';
|
|
97
|
-
const { warn, breach, edges = 'strict' } = thresholds;
|
|
98
|
-
const beyond = direction === 'higher-is-worse'
|
|
99
|
-
? (edge) => (edges === 'inclusive' ? value >= edge : value > edge)
|
|
100
|
-
: (edge) => (edges === 'inclusive' ? value <= edge : value < edge);
|
|
101
|
-
if (beyond(breach))
|
|
102
|
-
return 'breach';
|
|
103
|
-
if (beyond(warn))
|
|
104
|
-
return 'warn';
|
|
105
|
-
return 'low';
|
|
106
|
-
}
|
|
107
|
-
/**
|
|
108
|
-
* An empty or whitespace-only string is not a name. `??` only skips null and
|
|
109
|
-
* undefined, so `ariaLabel=""` or `bandLabels={{ breach: '' }}` won the fallback
|
|
110
|
-
* chain and produced an EMPTY accessible name — worse than the missing one it
|
|
111
|
-
* was meant to fix, because `aria-label=""` also suppresses the element's other
|
|
112
|
-
* naming routes. Normalizing to undefined lets the chain fall through properly.
|
|
113
|
-
*/
|
|
114
|
-
function nonEmpty(value) {
|
|
115
|
-
return value != null && value.trim() !== '' ? value : undefined;
|
|
116
|
-
}
|
|
117
|
-
/** Clamp a value into [min, max] then normalize to a 0..1 fraction of the
|
|
118
|
-
* track. A zero-width track (max <= min) collapses to 0 rather than dividing by
|
|
119
|
-
* zero. */
|
|
120
|
-
function trackFraction(value, min, max) {
|
|
121
|
-
if (!Number.isFinite(value) || max <= min)
|
|
122
|
-
return 0;
|
|
123
|
-
const clamped = Math.max(min, Math.min(max, value));
|
|
124
|
-
return (clamped - min) / (max - min);
|
|
125
|
-
}
|
|
126
|
-
/** Format the value for the mono readout per `format`. Money routes through
|
|
127
|
-
* MoneyText (never a hand-rolled string); the rest through the format helpers. */
|
|
128
|
-
function readout(value, format, currency, locale) {
|
|
129
|
-
if (!Number.isFinite(value))
|
|
130
|
-
return NO_VALUE;
|
|
131
|
-
if (format === 'money') {
|
|
132
|
-
return (_jsx(MoneyText, { amount: value, currency: currency, signColor: false, locale: locale }));
|
|
133
|
-
}
|
|
134
|
-
if (format === 'percent')
|
|
135
|
-
return formatPercent(value, { unit: 'percent', locale });
|
|
136
|
-
if (format === 'ratio')
|
|
137
|
-
return formatPercent(value, { unit: 'ratio', locale });
|
|
138
|
-
return formatCount(value, locale);
|
|
139
|
-
}
|
|
140
|
-
export function ThresholdGauge({ value, displayValue, min = 0, max, warn, breach, edges, direction, format = 'percent', currency, locale, label, ariaLabel, bandLabels, className }) {
|
|
141
|
-
const band = gaugeBand(value, { warn, breach, edges }, direction);
|
|
142
|
-
const { Icon, tint } = BAND[band];
|
|
143
|
-
// The sr-only word is the band's only non-chromatic cue, so it is also the
|
|
144
|
-
// string a trilingual consumer has to translate. pt-BR default, override wins.
|
|
145
|
-
// Every rung of the naming chain is normalized: an empty override must fall
|
|
146
|
-
// through to the next source rather than win with a blank string.
|
|
147
|
-
const word = nonEmpty(bandLabels?.[band]) ?? BAND[band].word;
|
|
148
|
-
const caption = nonEmpty(label);
|
|
149
|
-
// A `role="meter"` with no accessible name is announced as a naked number.
|
|
150
|
-
// The band word is always present and always meaningful, so the meter is named
|
|
151
|
-
// even when a consumer renders the gauge without a visible caption.
|
|
152
|
-
const meterName = nonEmpty(ariaLabel) ?? caption ?? word;
|
|
153
|
-
const valuePct = trackFraction(value, min, max) * 100;
|
|
154
|
-
const warnPct = trackFraction(warn, min, max) * 100;
|
|
155
|
-
const breachPct = trackFraction(breach, min, max) * 100;
|
|
156
|
-
return (_jsxs("div", { className: cn('flex flex-col gap-2', className), children: [_jsxs("div", { className: "flex items-baseline justify-between gap-3", children: [caption != null ? (_jsx("span", { className: LABEL_VOICE_CLASS, children: caption })) : (_jsx("span", {})), _jsxs("span", { className: cn('inline-flex items-center gap-1.5', tint), children: [_jsx(Icon, { "aria-hidden": true, className: "size-4 shrink-0" }), _jsxs("span", { className: "sr-only", children: [word, ": "] }), _jsx(Figure, { size: "row", className: tint, children: readout(displayValue ?? value, format, currency, locale) })] })] }), _jsxs("div", { role: "meter", "aria-valuenow": Number.isFinite(value) ? value : undefined, "aria-valuemin": min, "aria-valuemax": max, "aria-label": meterName, className: "border-border bg-muted relative h-2 w-full overflow-hidden rounded-full border", children: [_jsx("div", { "aria-hidden": true, className: cn('absolute inset-y-0 left-0 rounded-full', BAND_FILL[band]), style: { width: `${valuePct}%` } }), _jsx("span", { "aria-hidden": true, className: "bg-system-alert absolute inset-y-0 w-px", style: { left: `${warnPct}%` } }), _jsx("span", { "aria-hidden": true, className: "bg-destructive absolute inset-y-0 w-px", style: { left: `${breachPct}%` } })] })] }));
|
|
157
|
-
}
|
|
1
|
+
import{jsx as s,jsxs as a}from"react/jsx-runtime";import{OctagonX as E,ShieldCheck as k,TriangleAlert as D}from"lucide-react";import{LABEL_VOICE_CLASS as M}from"../../lib/typography.js";import{cn as m}from"../../lib/utils.js";import{Figure as O}from"../figure/index.js";import{formatCount as S,formatPercent as w,NO_VALUE as T}from"../format.js";import{MoneyText as $}from"../money-text/index.js";const y={low:{Icon:k,tint:"text-system-success-h1a",word:"Dentro do limite"},warn:{Icon:D,tint:"text-system-alert-h1a",word:"Pr\xF3ximo do limite"},breach:{Icon:E,tint:"text-system-error-h1a",word:"Limite ultrapassado"}};const z={low:"bg-system-success",warn:"bg-system-alert",breach:"bg-destructive"};export function gaugeBand(e,r,n){if(!Number.isFinite(e))return"low";const{warn:t,breach:c,edges:o="strict"}=r;const l=n==="higher-is-worse"?i=>o==="inclusive"?e>=i:e>i:i=>o==="inclusive"?e<=i:e<i;if(l(c))return"breach";if(l(t))return"warn";return"low"}function f(e){return e!=null&&e.trim()!==""?e:void 0}function h(e,r,n){if(!Number.isFinite(e)||n<=r)return 0;const t=Math.max(r,Math.min(n,e));return(t-r)/(n-r)}function V(e,r,n,t){if(!Number.isFinite(e))return T;if(r==="money"){return s($,{amount:e,currency:n,signColor:false,locale:t})}if(r==="percent")return w(e,{unit:"percent",locale:t});if(r==="ratio")return w(e,{unit:"ratio",locale:t});return S(e,t)}export function ThresholdGauge({value:e,displayValue:r,min:n=0,max:t,warn:c,breach:o,edges:l,direction:i,format:N="percent",currency:g,locale:x,label:F,ariaLabel:L,bandLabels:A,className:I}){const d=gaugeBand(e,{warn:c,breach:o,edges:l},i);const{Icon:_,tint:p}=y[d];const b=f(A?.[d])??y[d].word;const u=f(F);const j=f(L)??u??b;const C=h(e,n,t)*100;const P=h(c,n,t)*100;const B=h(o,n,t)*100;return a("div",{className:m("flex flex-col gap-2",I),children:[a("div",{className:"flex items-baseline justify-between gap-3",children:[u!=null?s("span",{className:M,children:u}):s("span",{}),a("span",{className:m("inline-flex items-center gap-1.5",p),children:[s(_,{"aria-hidden":true,className:"size-4 shrink-0"}),a("span",{className:"sr-only",children:[b,": "]}),s(O,{size:"row",className:p,children:V(r??e,N,g,x)})]})]}),a("div",{role:"meter","aria-valuenow":Number.isFinite(e)?e:void 0,"aria-valuemin":n,"aria-valuemax":t,"aria-label":j,className:"border-border bg-muted relative h-2 w-full overflow-hidden rounded-full border",children:[s("div",{"aria-hidden":true,className:m("absolute inset-y-0 left-0 rounded-full",z[d]),style:{width:`${C}%`}}),s("span",{"aria-hidden":true,className:"bg-system-alert absolute inset-y-0 w-px",style:{left:`${P}%`}}),s("span",{"aria-hidden":true,className:"bg-destructive absolute inset-y-0 w-px",style:{left:`${B}%`}})]})]})}
|
|
@@ -1,21 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { cn } from '../../lib/utils.js';
|
|
3
|
-
/** Container tint + border per tone. The title color is applied separately so
|
|
4
|
-
* the body copy stays high-contrast foreground. */
|
|
5
|
-
const TONE_CONTAINER = {
|
|
6
|
-
destructive: 'border-system-error-border bg-system-error-surface',
|
|
7
|
-
warning: 'border-system-alert-border bg-system-alert-surface',
|
|
8
|
-
info: 'border-system-info-border bg-system-info-surface',
|
|
9
|
-
success: 'border-system-success-border bg-system-success-surface',
|
|
10
|
-
neutral: 'border-border bg-muted/50'
|
|
11
|
-
};
|
|
12
|
-
const TONE_TITLE = {
|
|
13
|
-
destructive: 'text-system-error-text',
|
|
14
|
-
warning: 'text-system-alert-text',
|
|
15
|
-
info: 'text-system-info-text',
|
|
16
|
-
success: 'text-system-success-text',
|
|
17
|
-
neutral: 'text-foreground'
|
|
18
|
-
};
|
|
19
|
-
export function AlertBanner({ tone = 'neutral', title, children, detail, icon, className, ...props }) {
|
|
20
|
-
return (_jsxs("div", { ...props, role: "alert", "data-tone": tone, className: cn('flex gap-3 rounded-md border px-4 py-3', TONE_CONTAINER[tone], className), children: [icon != null ? (_jsx("span", { className: cn('mt-0.5 shrink-0', TONE_TITLE[tone]), "aria-hidden": true, children: icon })) : null, _jsxs("div", { className: "min-w-0 flex-1 space-y-1", children: [title != null ? (_jsx("p", { className: cn('text-sm font-medium', TONE_TITLE[tone]), children: title })) : null, children != null ? (_jsx("div", { className: "text-foreground text-xs leading-relaxed", children: children })) : null, detail != null ? (_jsx("pre", { className: "text-muted-foreground overflow-x-auto font-mono text-xs break-words whitespace-pre-wrap", children: detail })) : null] })] }));
|
|
21
|
-
}
|
|
1
|
+
import{jsx as r,jsxs as l}from"react/jsx-runtime";import{cn as s}from"../../lib/utils.js";const m={destructive:"border-system-error-border bg-system-error-surface",warning:"border-system-alert-border bg-system-alert-surface",info:"border-system-info-border bg-system-info-surface",success:"border-system-success-border bg-system-success-surface",neutral:"border-border bg-muted/50"};const d={destructive:"text-system-error-text",warning:"text-system-alert-text",info:"text-system-info-text",success:"text-system-success-text",neutral:"text-foreground"};export function AlertBanner({tone:e="neutral",title:t,children:n,detail:a,icon:o,className:u,...c}){return l("div",{...c,role:"alert","data-tone":e,className:s("flex gap-3 rounded-md border px-4 py-3",m[e],u),children:[o!=null?r("span",{className:s("mt-0.5 shrink-0",d[e]),"aria-hidden":true,children:o}):null,l("div",{className:"min-w-0 flex-1 space-y-1",children:[t!=null?r("p",{className:s("text-sm font-medium",d[e]),children:t}):null,n!=null?r("div",{className:"text-foreground text-xs leading-relaxed",children:n}):null,a!=null?r("pre",{className:"text-muted-foreground overflow-x-auto font-mono text-xs break-words whitespace-pre-wrap",children:a}):null]})]})}
|
|
@@ -1,51 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
/**
|
|
4
|
-
* AppShell — the console frame: a sidebar rail, an optional sticky header row,
|
|
5
|
-
* and a centered, scrollable content column. This is the whole screen, not a
|
|
6
|
-
* fragment, so it deliberately OWNS the SidebarProvider — the consumer drops a
|
|
7
|
-
* sindarian-ui sidebar (`SidebarRoot` + its parts) into `sidebar` and a
|
|
8
|
-
* `SidebarExpandButton` into `header`, and the collapse behavior comes free,
|
|
9
|
-
* no extra wrapper required.
|
|
10
|
-
*
|
|
11
|
-
* Landmark contract (the a11y-correct frame):
|
|
12
|
-
* SidebarProvider
|
|
13
|
-
* div (the shell ROW — carries `className`)
|
|
14
|
-
* a.sr-only (skip link, if labelled) // FIRST focusable in the document,
|
|
15
|
-
* // targets <main> — bypasses the nav
|
|
16
|
-
* {sidebar}
|
|
17
|
-
* div[data-slot=sidebar-inset] // the inset COLUMN — a plain div
|
|
18
|
-
* header (banner, if given) // SIBLING of <main>, not nested
|
|
19
|
-
* main#<id> tabIndex=-1 // the ONE main: skip-linkable + ref-able
|
|
20
|
-
* div (centered content container) → {children}
|
|
21
|
-
*
|
|
22
|
-
* The skip link is the FIRST child inside the shell row — before the sidebar —
|
|
23
|
-
* so a keyboard user's very first Tab lands on it and Enter jumps past the
|
|
24
|
-
* entire nav to the content <main>. That is the whole point of WCAG 2.4.1
|
|
25
|
-
* "Bypass Blocks": a skip link placed AFTER the nav would be reached only after
|
|
26
|
-
* tabbing through every nav item, defeating its purpose. The inset column is a
|
|
27
|
-
* <div>, not a <main>, so AppShell can own the single inner <main> for the
|
|
28
|
-
* content. The page header passed via `header` becomes a top-level banner
|
|
29
|
-
* SIBLING of <main> (one banner); a PageHeader rendered inside `children` lands
|
|
30
|
-
* inside <main> and is content, not a top-level landmark.
|
|
31
|
-
*
|
|
32
|
-
* Client by construction: it calls React.useId() to wire the skip link to the
|
|
33
|
-
* content <main>, and forwards a ref to that <main> so a consumer can focus it
|
|
34
|
-
* on route change — both are client concerns, hence the "use client" directive.
|
|
35
|
-
*/
|
|
36
|
-
import * as React from 'react';
|
|
37
|
-
import { SidebarProvider } from '../../components/ui/sidebar/index.js';
|
|
38
|
-
import { cn } from '../../lib/utils.js';
|
|
39
|
-
export const AppShell = React.forwardRef(function AppShell({ sidebar, header, headerLabel, children, className, contentClassName, skipToContentLabel }, ref) {
|
|
40
|
-
// The content <main> id — wires the skip link to its target and gives the
|
|
41
|
-
// <main> a stable, focusable anchor. Internal: no consumer-facing id prop.
|
|
42
|
-
const mainId = React.useId();
|
|
43
|
-
// sindarian-ui's SidebarProvider is context-only (it renders no DOM and
|
|
44
|
-
// takes no className), so the shell owns the flex row itself and `className`
|
|
45
|
-
// lands on it — the same element the legacy provider's className reached.
|
|
46
|
-
return (_jsx(SidebarProvider, { children: _jsxs("div", { className: cn('flex h-svh w-full overflow-hidden', className), children: [skipToContentLabel ? (_jsx("a", { href: `#${mainId}`, className: "focus:bg-background focus:text-foreground sr-only focus:not-sr-only focus:absolute focus:z-50 focus:m-2 focus:rounded focus:px-3 focus:py-2 focus:shadow", children: skipToContentLabel })) : null, sidebar, _jsxs("div", { "data-slot": "sidebar-inset", className: "bg-background relative flex w-full min-w-0 flex-1 flex-col", children: [header ? (_jsx("header", { "aria-label": headerLabel, className: "border-border bg-background sticky top-0 z-10 flex items-center gap-3 border-b px-6 py-3", children: header })) : null, _jsx("main", { id: mainId, ref: ref, tabIndex: -1,
|
|
47
|
-
// min-h-0 lets this flex child shrink below its content height —
|
|
48
|
-
// without it `flex-1` keeps min-height:auto and nothing scrolls.
|
|
49
|
-
className: "min-h-0 flex-1 overflow-y-auto outline-none", children: _jsx("div", { className: cn('mx-auto w-full max-w-7xl px-6 py-8 lg:px-8', contentClassName), children: children }) })] })] }) }));
|
|
50
|
-
});
|
|
51
|
-
AppShell.displayName = 'AppShell';
|
|
1
|
+
"use client";import{jsx as e,jsxs as s}from"react/jsx-runtime";import*as a from"react";import{SidebarProvider as m}from"../../components/ui/sidebar/index.js";import{cn as c}from"../../lib/utils.js";export const AppShell=a.forwardRef(function p({sidebar:n,header:l,headerLabel:d,children:f,className:i,contentClassName:u,skipToContentLabel:o},t){const r=a.useId();return e(m,{children:s("div",{className:c("flex h-svh w-full overflow-hidden",i),children:[o?e("a",{href:`#${r}`,className:"focus:bg-background focus:text-foreground sr-only focus:not-sr-only focus:absolute focus:z-50 focus:m-2 focus:rounded focus:px-3 focus:py-2 focus:shadow",children:o}):null,n,s("div",{"data-slot":"sidebar-inset",className:"bg-background relative flex w-full min-w-0 flex-1 flex-col",children:[l?e("header",{"aria-label":d,className:"border-border bg-background sticky top-0 z-10 flex items-center gap-3 border-b px-6 py-3",children:l}):null,e("main",{id:r,ref:t,tabIndex:-1,className:"min-h-0 flex-1 overflow-y-auto outline-none",children:e("div",{className:c("mx-auto w-full max-w-7xl px-6 py-8 lg:px-8",u),children:f})})]})]})})});AppShell.displayName="AppShell";
|
|
@@ -1,23 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
/**
|
|
4
|
-
* CursorPager drives the cursor-pagination envelope
|
|
5
|
-
* { items, hasMore, nextCursor, prevCursor, limit }
|
|
6
|
-
* shared by the console's cursor-paged list endpoints (do not use it on
|
|
7
|
-
* offset-based endpoints).
|
|
8
|
-
*
|
|
9
|
-
* It is controlled: the parent owns the active cursor (the value it passes to
|
|
10
|
-
* the list hook) and reacts to `onCursorChange`. Pass the page envelope's
|
|
11
|
-
* `hasMore`, `nextCursor`, `prevCursor`. "Previous" is enabled when the page
|
|
12
|
-
* reports a `prevCursor`; "Next" when `hasMore` and a `nextCursor` exist.
|
|
13
|
-
*
|
|
14
|
-
* `onCursorChange(undefined)` requests the first page (no cursor).
|
|
15
|
-
*/
|
|
16
|
-
import { ChevronLeft, ChevronRight } from 'lucide-react';
|
|
17
|
-
import { Button } from '../../components/ui/button/index.js';
|
|
18
|
-
import { cn } from '../../lib/utils.js';
|
|
19
|
-
export function CursorPager({ hasMore, nextCursor, prevCursor, onCursorChange, disabled = false, summary, previousLabel = 'Previous', nextLabel = 'Next', className }) {
|
|
20
|
-
const canPrev = !disabled && !!prevCursor;
|
|
21
|
-
const canNext = !disabled && !!hasMore && !!nextCursor;
|
|
22
|
-
return (_jsxs("div", { className: cn('flex items-center justify-between gap-4 pt-3', className), children: [_jsx("p", { className: "text-muted-foreground text-xs", children: summary ?? '' }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Button, { type: "button", variant: "outline", size: "small", icon: _jsx(ChevronLeft, { "aria-hidden": true }), iconPlacement: "start", disabled: !canPrev, onClick: () => onCursorChange(prevCursor ?? undefined), children: previousLabel }), _jsx(Button, { type: "button", variant: "outline", size: "small", icon: _jsx(ChevronRight, { "aria-hidden": true }), iconPlacement: "end", disabled: !canNext, onClick: () => onCursorChange(nextCursor ?? undefined), children: nextLabel })] })] }));
|
|
23
|
-
}
|
|
1
|
+
"use client";import{jsx as e,jsxs as r}from"react/jsx-runtime";import{ChevronLeft as x,ChevronRight as p}from"lucide-react";import{Button as s}from"../../components/ui/button/index.js";import{cn as h}from"../../lib/utils.js";export function CursorPager({hasMore:c,nextCursor:t,prevCursor:n,onCursorChange:i,disabled:a=false,summary:o,previousLabel:l="Previous",nextLabel:d="Next",className:m}){const u=!a&&!!n;const f=!a&&!!c&&!!t;return r("div",{className:h("flex items-center justify-between gap-4 pt-3",m),children:[e("p",{className:"text-muted-foreground text-xs",children:o??""}),r("div",{className:"flex items-center gap-2",children:[e(s,{type:"button",variant:"outline",size:"small",icon:e(x,{"aria-hidden":true}),iconPlacement:"start",disabled:!u,onClick:()=>i(n??void 0),children:l}),e(s,{type:"button",variant:"outline",size:"small",icon:e(p,{"aria-hidden":true}),iconPlacement:"end",disabled:!f,onClick:()=>i(t??void 0),children:d})]})]})}
|