@lerianstudio/sindarian-ui 2.0.0-beta.25 → 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.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.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.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/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.js +1 -80
- 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/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/separator/index.js +1 -43
- 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.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.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.js +1 -14
- 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.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.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.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.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.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.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.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.js +1 -11
- 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/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/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,22 +1 @@
|
|
|
1
|
-
|
|
2
|
-
//
|
|
3
|
-
// The composed enterprise layer absorbed from @lerianstudio/sindarian-x@0.15.0.
|
|
4
|
-
// Outer public APIs are byte-compatible with that package; internals are
|
|
5
|
-
// recomposed over sindarian-ui primitives and its design tokens.
|
|
6
|
-
//
|
|
7
|
-
// Names already exported by sindarian-ui are NEVER re-exported here (FC-4):
|
|
8
|
-
// consumers adapt to sindarian-ui's API for Button, Badge, Table*, Sidebar*,
|
|
9
|
-
// EntityBox*, Stepper*, PageHeader, ConfirmationDialog and friends.
|
|
10
|
-
export { AlertBanner } from './alert-banner/index.js';
|
|
11
|
-
export { AppShell } from './app-shell/index.js';
|
|
12
|
-
export { CursorPager } from './cursor-pager/index.js';
|
|
13
|
-
export { DataTable } from './data-table/index.js';
|
|
14
|
-
export { DateRangePicker } from './date-range-picker/index.js';
|
|
15
|
-
export { DetailPanel } from './detail-panel/index.js';
|
|
16
|
-
export { EmptyState } from './empty-state/index.js';
|
|
17
|
-
export { NumberInput } from './number-input/index.js';
|
|
18
|
-
export { SearchInput } from './search-input/index.js';
|
|
19
|
-
export { StatCard } from './stat-card/index.js';
|
|
20
|
-
export { DEFAULT_STATUS_VARIANTS, StatusBadge } from './status-badge/index.js';
|
|
21
|
-
export { VirtualizedTable } from './virtualized-table/index.js';
|
|
22
|
-
export { useIsMobile } from './use-is-mobile.js';
|
|
1
|
+
export{AlertBanner}from"./alert-banner/index.js";export{AppShell}from"./app-shell/index.js";export{CursorPager}from"./cursor-pager/index.js";export{DataTable}from"./data-table/index.js";export{DateRangePicker}from"./date-range-picker/index.js";export{DetailPanel}from"./detail-panel/index.js";export{EmptyState}from"./empty-state/index.js";export{NumberInput}from"./number-input/index.js";export{SearchInput}from"./search-input/index.js";export{StatCard}from"./stat-card/index.js";export{DEFAULT_STATUS_VARIANTS,StatusBadge}from"./status-badge/index.js";export{VirtualizedTable}from"./virtualized-table/index.js";export{useIsMobile}from"./use-is-mobile.js";
|
|
@@ -1,214 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
/**
|
|
4
|
-
* NumberInput — a controlled numeric input with +/- steppers.
|
|
5
|
-
*
|
|
6
|
-
* Composes sindarian-ui's Input (the text field) and IconButton (the two
|
|
7
|
-
* steppers). The external contract is a plain `number | null` (null = empty).
|
|
8
|
-
* Steppers step by `step` and clamp the result to [min, max]. While focused the
|
|
9
|
-
* field shows the raw, editable string the user is typing; on blur it reformats
|
|
10
|
-
* the committed number via Intl.NumberFormat(locale, { maximumFractionDigits:
|
|
11
|
-
* precision }) (e.g. 1,234.50). Parsing always emits the raw number, so digits
|
|
12
|
-
* the user types are never silently rewritten mid-edit.
|
|
13
|
-
*/
|
|
14
|
-
import * as React from 'react';
|
|
15
|
-
import { Minus, Plus } from 'lucide-react';
|
|
16
|
-
import { safeLocale } from '../../domain/format.js';
|
|
17
|
-
import { IconButton } from '../../components/ui/icon-button/index.js';
|
|
18
|
-
import { Input } from '../../components/ui/input/index.js';
|
|
19
|
-
import { cn } from '../../lib/utils.js';
|
|
20
|
-
function clamp(value, min, max) {
|
|
21
|
-
let next = value;
|
|
22
|
-
if (typeof min === 'number' && next < min)
|
|
23
|
-
next = min;
|
|
24
|
-
if (typeof max === 'number' && next > max)
|
|
25
|
-
next = max;
|
|
26
|
-
return next;
|
|
27
|
-
}
|
|
28
|
-
/**
|
|
29
|
-
* `Number.prototype.toFixed` rejects more than 100 fraction digits. Denormals
|
|
30
|
-
* genuinely need more (`decimalPlaces(5e-324)` is 324), so this is a RangeError
|
|
31
|
-
* guard, not a precision policy — no step a person types comes close.
|
|
32
|
-
*/
|
|
33
|
-
const MAX_FIXED_DIGITS = 100;
|
|
34
|
-
/**
|
|
35
|
-
* Fraction digits a number carries, reading exponent notation correctly.
|
|
36
|
-
* `String(1e-7)` is `'1e-7'`, which has no '.' at all, so counting characters
|
|
37
|
-
* after a decimal point reports 0 and silently kills any step below 1e-6.
|
|
38
|
-
* `toExponential()` gives a canonical mantissa/exponent pair instead:
|
|
39
|
-
* 1e-7 -> mantissa '1', exponent -7 -> 7 fraction digits.
|
|
40
|
-
*/
|
|
41
|
-
function decimalPlaces(value) {
|
|
42
|
-
if (!Number.isFinite(value) || Number.isInteger(value))
|
|
43
|
-
return 0;
|
|
44
|
-
const [mantissa, exponent] = value.toExponential().split('e');
|
|
45
|
-
const mantissaDigits = (mantissa.split('.')[1] ?? '').length;
|
|
46
|
-
return Math.max(0, mantissaDigits - Number(exponent));
|
|
47
|
-
}
|
|
48
|
-
/**
|
|
49
|
-
* Snap a stepped result to the fraction digits its own operands carry, so
|
|
50
|
-
* binary float drift never escapes to the parent: stepping 0.1 by 0.2 must
|
|
51
|
-
* commit 0.3, not 0.30000000000000004.
|
|
52
|
-
*
|
|
53
|
-
* Anchoring on the OPERANDS is what makes this safe at both ends of the range.
|
|
54
|
-
* Rounding to a fixed number of significant digits cannot work: at 1e15 the
|
|
55
|
-
* 15th significant digit is the units place, so 1e15 + 0.5 would snap straight
|
|
56
|
-
* back to 1e15 and the stepper would die. Nor can a "is this change small
|
|
57
|
-
* enough to be noise?" tolerance decide it — 1e15 + 0.125 is exactly
|
|
58
|
-
* representable and must be kept, yet its relative distance from the rounded
|
|
59
|
-
* form (1.25e-16) is SMALLER than genuine drift in 0.1 + 0.2 (1.85e-16), so no
|
|
60
|
-
* threshold separates the two cases. The operands do: `1e15` and `0.5` need one
|
|
61
|
-
* fraction digit between them, `0.1` and `0.2` also one, and rounding each
|
|
62
|
-
* result to its own digit count keeps the first and repairs the second.
|
|
63
|
-
*
|
|
64
|
-
* `toFixed` (not `value * 10 ** digits`) does the rounding: the multiply
|
|
65
|
-
* overflows Number.MAX_SAFE_INTEGER for large values and hands back the very
|
|
66
|
-
* drift it was meant to remove.
|
|
67
|
-
*/
|
|
68
|
-
function snapToStep(value, base, step) {
|
|
69
|
-
if (!Number.isFinite(value))
|
|
70
|
-
return value;
|
|
71
|
-
const digits = Math.min(MAX_FIXED_DIGITS, Math.max(decimalPlaces(base), decimalPlaces(step)));
|
|
72
|
-
return Number(value.toFixed(digits));
|
|
73
|
-
}
|
|
74
|
-
/** The locale's decimal and group separators, derived from a known sample. */
|
|
75
|
-
function localeSeparators(locale) {
|
|
76
|
-
const parts = new Intl.NumberFormat(safeLocale(locale)).formatToParts(11111.1);
|
|
77
|
-
const decimal = parts.find((p) => p.type === 'decimal')?.value ?? '.';
|
|
78
|
-
const group = parts.find((p) => p.type === 'group')?.value ?? '';
|
|
79
|
-
return { decimal, group };
|
|
80
|
-
}
|
|
81
|
-
/**
|
|
82
|
-
* Parse the editable string into a number, round-tripping the component's
|
|
83
|
-
* `locale`: strip the locale group separators, swap the locale decimal mark to
|
|
84
|
-
* '.', then `Number()`. Null when empty/unparseable.
|
|
85
|
-
*
|
|
86
|
-
* Internal (not part of the public barrel) but exported for its unit tests —
|
|
87
|
-
* numeric-entry correctness is the load-bearing behavior here.
|
|
88
|
-
*/
|
|
89
|
-
export function parseRaw(raw, locale) {
|
|
90
|
-
const trimmed = raw.trim();
|
|
91
|
-
if (trimmed === '')
|
|
92
|
-
return null;
|
|
93
|
-
const { decimal, group } = localeSeparators(locale);
|
|
94
|
-
let normalized = trimmed;
|
|
95
|
-
if (group)
|
|
96
|
-
normalized = normalized.split(group).join('');
|
|
97
|
-
if (decimal !== '.')
|
|
98
|
-
normalized = normalized.split(decimal).join('.');
|
|
99
|
-
if (normalized === '')
|
|
100
|
-
return null;
|
|
101
|
-
const parsed = Number(normalized);
|
|
102
|
-
// NaN is not the only unusable result: `Number('1e999')` and `Number('Infinity')`
|
|
103
|
-
// are both `Infinity`, which passed the NaN check and was committed straight to
|
|
104
|
-
// `onValueChange`. An infinite committed value poisons everything downstream —
|
|
105
|
-
// `clamp` returns it verbatim with no `max`, `toFixed` cannot render it, and a
|
|
106
|
-
// form submits it. A non-finite entry is no number at all, so the committed
|
|
107
|
-
// value stays where it was.
|
|
108
|
-
return Number.isFinite(parsed) ? parsed : null;
|
|
109
|
-
}
|
|
110
|
-
/**
|
|
111
|
-
* A transient partial entry that should not commit yet — a lone sign, a lone
|
|
112
|
-
* decimal mark, sign+decimal, or a value ending in the decimal mark (the user
|
|
113
|
-
* is still typing). Suppressing these keeps the external value from
|
|
114
|
-
* round-tripping through `null` mid-edit.
|
|
115
|
-
*
|
|
116
|
-
* Internal (not part of the public barrel) but exported for its unit tests.
|
|
117
|
-
*/
|
|
118
|
-
export function isTransientPartial(raw, locale) {
|
|
119
|
-
const trimmed = raw.trim();
|
|
120
|
-
if (trimmed === '')
|
|
121
|
-
return false;
|
|
122
|
-
const { decimal } = localeSeparators(locale);
|
|
123
|
-
const d = decimal.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
124
|
-
return new RegExp(`^-?(${d})?$|${d}$`).test(trimmed);
|
|
125
|
-
}
|
|
126
|
-
export function NumberInput({ value, onValueChange, min, max, step = 1, precision, locale, disabled = false, id, className, placeholder, 'aria-label': ariaLabel, 'aria-describedby': ariaDescribedBy }) {
|
|
127
|
-
const [focused, setFocused] = React.useState(false);
|
|
128
|
-
// The string the user is editing. Only authoritative while focused.
|
|
129
|
-
const [draft, setDraft] = React.useState('');
|
|
130
|
-
// A step has to be a POSITIVE FINITE number or the control stops meaning what
|
|
131
|
-
// its buttons say: a negative step makes "+" decrease and "−" increase, a zero
|
|
132
|
-
// or NaN step commits a value that never progresses (and NaN escapes into the
|
|
133
|
-
// form), and an infinite step commits Infinity the moment `max` is unset.
|
|
134
|
-
// None of those is a step, so an unusable one falls back to the documented
|
|
135
|
-
// default of 1 rather than being fed to the arithmetic.
|
|
136
|
-
const safeStep = Number.isFinite(step) && step > 0 ? step : 1;
|
|
137
|
-
const formatter = React.useMemo(() => new Intl.NumberFormat(safeLocale(locale), {
|
|
138
|
-
maximumFractionDigits: precision ?? 20,
|
|
139
|
-
minimumFractionDigits: precision ?? 0,
|
|
140
|
-
useGrouping: true
|
|
141
|
-
}), [locale, precision]);
|
|
142
|
-
// Blurred display: locale-formatted committed value, or empty.
|
|
143
|
-
const displayValue = focused
|
|
144
|
-
? draft
|
|
145
|
-
: value === null
|
|
146
|
-
? ''
|
|
147
|
-
: formatter.format(value);
|
|
148
|
-
const commit = (next) => {
|
|
149
|
-
onValueChange(next);
|
|
150
|
-
};
|
|
151
|
-
const stepBy = (direction) => {
|
|
152
|
-
const base = value ?? 0;
|
|
153
|
-
// Snap FIRST, then clamp — never the other way round. Snapping rounds to
|
|
154
|
-
// the operands' fraction digits, so snapping a CLAMPED value rounds the
|
|
155
|
-
// bound itself: stepping 0.2 by 0.1 under max 0.25 clamps to 0.25, which
|
|
156
|
-
// then snaps to one digit as 0.3 and escapes the maximum. Clamping last
|
|
157
|
-
// makes the bound the final word, and `clamp` returns it verbatim, so a
|
|
158
|
-
// bound is emitted exactly as the caller gave it.
|
|
159
|
-
const next = clamp(snapToStep(base + direction * safeStep, base, safeStep), min, max);
|
|
160
|
-
commit(next);
|
|
161
|
-
// Seed the draft via the locale formatter so a stepper press doesn't flip
|
|
162
|
-
// the user's separators (e.g. ',' → '.').
|
|
163
|
-
if (focused)
|
|
164
|
-
setDraft(formatter.format(next));
|
|
165
|
-
};
|
|
166
|
-
const onChange = (event) => {
|
|
167
|
-
const raw = event.target.value;
|
|
168
|
-
setDraft(raw);
|
|
169
|
-
// Commit only a genuine clear or a parsed number; suppress transient
|
|
170
|
-
// partials ("-", ".", "-.", trailing separator) so typing a negative or
|
|
171
|
-
// decimal doesn't round-trip the external value through null mid-entry.
|
|
172
|
-
if (raw.trim() === '') {
|
|
173
|
-
commit(null);
|
|
174
|
-
return;
|
|
175
|
-
}
|
|
176
|
-
if (isTransientPartial(raw, locale))
|
|
177
|
-
return;
|
|
178
|
-
const parsed = parseRaw(raw, locale);
|
|
179
|
-
if (parsed !== null)
|
|
180
|
-
commit(parsed);
|
|
181
|
-
};
|
|
182
|
-
// Keyboard stepping (WCAG 2.1.1): the steppers are tabIndex={-1}, so the
|
|
183
|
-
// input owns keyboard interaction per the ARIA spinbutton pattern.
|
|
184
|
-
const onKeyDown = (event) => {
|
|
185
|
-
if (disabled)
|
|
186
|
-
return;
|
|
187
|
-
if (event.key === 'ArrowUp') {
|
|
188
|
-
event.preventDefault();
|
|
189
|
-
stepBy(1);
|
|
190
|
-
}
|
|
191
|
-
else if (event.key === 'ArrowDown') {
|
|
192
|
-
event.preventDefault();
|
|
193
|
-
stepBy(-1);
|
|
194
|
-
}
|
|
195
|
-
};
|
|
196
|
-
const onFocus = () => {
|
|
197
|
-
setFocused(true);
|
|
198
|
-
// Seed via the locale formatter (not String(value), which forces '.') so
|
|
199
|
-
// focusing doesn't flip the separator the user expects.
|
|
200
|
-
setDraft(value === null ? '' : formatter.format(value));
|
|
201
|
-
};
|
|
202
|
-
const onBlur = () => {
|
|
203
|
-
setFocused(false);
|
|
204
|
-
// Re-clamp the committed value so a typed out-of-range entry settles.
|
|
205
|
-
if (value !== null) {
|
|
206
|
-
const clamped = clamp(value, min, max);
|
|
207
|
-
if (clamped !== value)
|
|
208
|
-
commit(clamped);
|
|
209
|
-
}
|
|
210
|
-
};
|
|
211
|
-
const atMin = typeof min === 'number' && value !== null && value <= min;
|
|
212
|
-
const atMax = typeof max === 'number' && value !== null && value >= max;
|
|
213
|
-
return (_jsxs("div", { className: cn('inline-flex items-stretch gap-1', className), children: [_jsx(IconButton, { className: "shrink-0", type: "button", variant: "outline", disabled: disabled || atMin, "aria-label": `Decrease${ariaLabel ? ` ${ariaLabel}` : ''}`, onClick: () => stepBy(-1), tabIndex: -1, children: _jsx(Minus, { className: "size-4", "aria-hidden": true }) }), _jsx("div", { className: "w-28 min-w-0", children: _jsx(Input, { id: id, inputMode: "decimal", role: "spinbutton", "aria-valuenow": value ?? undefined, "aria-valuemin": min, "aria-valuemax": max, className: "text-right tabular-nums", value: displayValue, placeholder: placeholder, disabled: disabled, "aria-label": ariaLabel, "aria-describedby": ariaDescribedBy, onChange: onChange, onKeyDown: onKeyDown, onFocus: onFocus, onBlur: onBlur }) }), _jsx(IconButton, { className: "shrink-0", type: "button", variant: "outline", disabled: disabled || atMax, "aria-label": `Increase${ariaLabel ? ` ${ariaLabel}` : ''}`, onClick: () => stepBy(1), tabIndex: -1, children: _jsx(Plus, { className: "size-4", "aria-hidden": true }) })] }));
|
|
214
|
-
}
|
|
1
|
+
"use client";import{jsx as c,jsxs as R}from"react/jsx-runtime";import*as g from"react";import{Minus as G,Plus as K}from"lucide-react";import{safeLocale as w}from"../../domain/format.js";import{IconButton as F}from"../../components/ui/icon-button/index.js";import{Input as X}from"../../components/ui/input/index.js";import{cn as U}from"../../lib/utils.js";function D(t,r,e){let n=t;if(typeof r==="number"&&n<r)n=r;if(typeof e==="number"&&n>e)n=e;return n}const V=100;function I(t){if(!Number.isFinite(t)||Number.isInteger(t))return 0;const[r,e]=t.toExponential().split("e");const n=(r.split(".")[1]??"").length;return Math.max(0,n-Number(e))}function q(t,r,e){if(!Number.isFinite(t))return t;const n=Math.min(V,Math.max(I(r),I(e)));return Number(t.toFixed(n))}function $(t){const r=new Intl.NumberFormat(w(t)).formatToParts(11111.1);const e=r.find(i=>i.type==="decimal")?.value??".";const n=r.find(i=>i.type==="group")?.value??"";return{decimal:e,group:n}}export function parseRaw(t,r){const e=t.trim();if(e==="")return null;const{decimal:n,group:i}=$(r);let o=e;if(i)o=o.split(i).join("");if(n!==".")o=o.split(n).join(".");if(o==="")return null;const a=Number(o);return Number.isFinite(a)?a:null}export function isTransientPartial(t,r){const e=t.trim();if(e==="")return false;const{decimal:n}=$(r);const i=n.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");return new RegExp(`^-?(${i})?$|${i}$`).test(e)}export function NumberInput({value:t,onValueChange:r,min:e,max:n,step:i=1,precision:o,locale:a,disabled:f=false,id:M,className:j,placeholder:k,"aria-label":l,"aria-describedby":S}){const[h,x]=g.useState(false);const[B,b]=g.useState("");const y=Number.isFinite(i)&&i>0?i:1;const N=g.useMemo(()=>new Intl.NumberFormat(w(a),{maximumFractionDigits:o??20,minimumFractionDigits:o??0,useGrouping:true}),[a,o]);const C=h?B:t===null?"":N.format(t);const p=s=>{r(s)};const d=s=>{const u=t??0;const m=D(q(u+s*y,u,y),e,n);p(m);if(h)b(N.format(m))};const P=s=>{const u=s.target.value;b(u);if(u.trim()===""){p(null);return}if(isTransientPartial(u,a))return;const m=parseRaw(u,a);if(m!==null)p(m)};const T=s=>{if(f)return;if(s.key==="ArrowUp"){s.preventDefault();d(1)}else if(s.key==="ArrowDown"){s.preventDefault();d(-1)}};const _=()=>{x(true);b(t===null?"":N.format(t))};const z=()=>{x(false);if(t!==null){const s=D(t,e,n);if(s!==t)p(s)}};const A=typeof e==="number"&&t!==null&&t<=e;const E=typeof n==="number"&&t!==null&&t>=n;return R("div",{className:U("inline-flex items-stretch gap-1",j),children:[c(F,{className:"shrink-0",type:"button",variant:"outline",disabled:f||A,"aria-label":`Decrease${l?` ${l}`:""}`,onClick:()=>d(-1),tabIndex:-1,children:c(G,{className:"size-4","aria-hidden":true})}),c("div",{className:"w-28 min-w-0",children:c(X,{id:M,inputMode:"decimal",role:"spinbutton","aria-valuenow":t??void 0,"aria-valuemin":e,"aria-valuemax":n,className:"text-right tabular-nums",value:C,placeholder:k,disabled:f,"aria-label":l,"aria-describedby":S,onChange:P,onKeyDown:T,onFocus:_,onBlur:z})}),c(F,{className:"shrink-0",type:"button",variant:"outline",disabled:f||E,"aria-label":`Increase${l?` ${l}`:""}`,onClick:()=>d(1),tabIndex:-1,children:c(K,{className:"size-4","aria-hidden":true})})]})}
|
|
@@ -1,85 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
-
/**
|
|
4
|
-
* SearchInput — the console filter row's text field.
|
|
5
|
-
*
|
|
6
|
-
* A controlled text field with a leading search glyph and a trailing clear
|
|
7
|
-
* button that appears only when there is text. It debounces the *emission*
|
|
8
|
-
* (`onValueChange`) while keeping the visible field perfectly responsive: a
|
|
9
|
-
* local draft mirrors the controlled `value`, every keystroke updates the draft
|
|
10
|
-
* immediately, and a trailing timer flushes the draft to the parent after
|
|
11
|
-
* `debounceMs`. Clearing flushes synchronously (no lingering timer fighting an
|
|
12
|
-
* explicit empty).
|
|
13
|
-
*
|
|
14
|
-
* Composes sindarian-ui's Input via its start/end adornment slots.
|
|
15
|
-
*/
|
|
16
|
-
import * as React from 'react';
|
|
17
|
-
import { Search, X } from 'lucide-react';
|
|
18
|
-
import { IconButton } from '../../components/ui/icon-button/index.js';
|
|
19
|
-
import { Input, InputAdornment } from '../../components/ui/input/index.js';
|
|
20
|
-
import { cn } from '../../lib/utils.js';
|
|
21
|
-
/**
|
|
22
|
-
* Marks "no emission of ours is awaiting its echo". A unique object, not a
|
|
23
|
-
* string: any string sentinel is a value a parent could legitimately pass.
|
|
24
|
-
*/
|
|
25
|
-
const NO_PENDING_ECHO = Symbol('no pending echo');
|
|
26
|
-
export function SearchInput({ value, onValueChange, debounceMs = 250, placeholder = 'Search…', 'aria-label': ariaLabel, clearLabel = 'Clear search', disabled = false, className, id, onKeyDown }) {
|
|
27
|
-
// Local draft keeps typing instant; the controlled `value` is the source of
|
|
28
|
-
// truth between edits. When the parent changes `value` out of band (reset,
|
|
29
|
-
// programmatic set), the draft follows.
|
|
30
|
-
const [draft, setDraft] = React.useState(value);
|
|
31
|
-
const timer = React.useRef(null);
|
|
32
|
-
// The value this component last emitted and has not yet seen echoed back. The
|
|
33
|
-
// parent echoes it one render later, by which time the user may already have
|
|
34
|
-
// typed further — syncing the draft to that echo would rewind the field and
|
|
35
|
-
// swallow those keystrokes. Starts as the sentinel rather than `value`, so the
|
|
36
|
-
// initial value is never mistaken for an echo of something we sent.
|
|
37
|
-
const lastEmitted = React.useRef(NO_PENDING_ECHO);
|
|
38
|
-
React.useEffect(() => {
|
|
39
|
-
const isEcho = lastEmitted.current === value;
|
|
40
|
-
// ANY external change consumes the token, echo or not. Consuming it only on
|
|
41
|
-
// a match left a DROPPED emission pending forever: emit "pix", the parent
|
|
42
|
-
// rejects it and holds "", the parent later sets "foo" (draft follows, token
|
|
43
|
-
// still holding "pix"), the parent then sets "pix" — read as our own echo,
|
|
44
|
-
// so the draft stayed on "foo" and the visible field disagreed with the
|
|
45
|
-
// controlled value. A later external change is proof the parent moved on,
|
|
46
|
-
// which invalidates any echo still outstanding.
|
|
47
|
-
lastEmitted.current = NO_PENDING_ECHO;
|
|
48
|
-
// A genuine echo must NOT reach the draft: the parent echoes one render
|
|
49
|
-
// later, by which time the user may have typed further, and syncing would
|
|
50
|
-
// rewind the field and swallow those keystrokes.
|
|
51
|
-
if (isEcho)
|
|
52
|
-
return;
|
|
53
|
-
setDraft(value);
|
|
54
|
-
}, [value]);
|
|
55
|
-
const clearTimer = React.useCallback(() => {
|
|
56
|
-
if (timer.current !== null) {
|
|
57
|
-
clearTimeout(timer.current);
|
|
58
|
-
timer.current = null;
|
|
59
|
-
}
|
|
60
|
-
}, []);
|
|
61
|
-
// Cancel any pending debounced emission on unmount — don't fire a stale
|
|
62
|
-
// search into a parent that's tearing down. (Clearing flushes synchronously
|
|
63
|
-
// via handleClear, so a real clear is never lost.)
|
|
64
|
-
React.useEffect(() => clearTimer, [clearTimer]);
|
|
65
|
-
const handleChange = (event) => {
|
|
66
|
-
const next = event.target.value;
|
|
67
|
-
setDraft(next);
|
|
68
|
-
clearTimer();
|
|
69
|
-
timer.current = setTimeout(() => {
|
|
70
|
-
timer.current = null;
|
|
71
|
-
lastEmitted.current = next;
|
|
72
|
-
onValueChange(next);
|
|
73
|
-
}, debounceMs);
|
|
74
|
-
};
|
|
75
|
-
const handleClear = () => {
|
|
76
|
-
clearTimer();
|
|
77
|
-
setDraft('');
|
|
78
|
-
lastEmitted.current = '';
|
|
79
|
-
onValueChange('');
|
|
80
|
-
};
|
|
81
|
-
const hasValue = draft.length > 0;
|
|
82
|
-
return (_jsx("div", { className: cn('relative w-full sm:w-64', className), children: _jsx(Input, { id: id, type: "search", role: "searchbox", value: draft, onChange: handleChange, onKeyDown: onKeyDown, placeholder: placeholder, "aria-label": ariaLabel ?? placeholder ?? 'Search', disabled: disabled,
|
|
83
|
-
// The native search clear glyph would duplicate our button.
|
|
84
|
-
className: "[&::-webkit-search-cancel-button]:appearance-none", startAdornment: _jsx(InputAdornment, { position: "start", children: _jsx(Search, { "aria-hidden": true }) }), endAdornment: hasValue ? (_jsx(InputAdornment, { position: "end", children: _jsx(IconButton, { type: "button", variant: "plain", size: "small", "aria-label": clearLabel, disabled: disabled, onClick: handleClear, className: "text-muted-foreground hover:text-foreground", children: _jsx(X, { className: "size-4", "aria-hidden": true }) }) })) : undefined }) }));
|
|
85
|
-
}
|
|
1
|
+
"use client";import{jsx as e}from"react/jsx-runtime";import*as t from"react";import{Search as w,X as y}from"lucide-react";import{IconButton as k}from"../../components/ui/icon-button/index.js";import{Input as v,InputAdornment as f}from"../../components/ui/input/index.js";import{cn as A}from"../../lib/utils.js";const h=Symbol("no pending echo");export function SearchInput({value:n,onValueChange:i,debounceMs:p=250,placeholder:u="Search\u2026","aria-label":b,clearLabel:x="Clear search",disabled:m=false,className:N,id:g,onKeyDown:C}){const[d,c]=t.useState(n);const r=t.useRef(null);const a=t.useRef(h);t.useEffect(()=>{const s=a.current===n;a.current=h;if(s)return;c(n)},[n]);const o=t.useCallback(()=>{if(r.current!==null){clearTimeout(r.current);r.current=null}},[]);t.useEffect(()=>o,[o]);const E=s=>{const l=s.target.value;c(l);o();r.current=setTimeout(()=>{r.current=null;a.current=l;i(l)},p)};const S=()=>{o();c("");a.current="";i("")};const I=d.length>0;return e("div",{className:A("relative w-full sm:w-64",N),children:e(v,{id:g,type:"search",role:"searchbox",value:d,onChange:E,onKeyDown:C,placeholder:u,"aria-label":b??u??"Search",disabled:m,className:"[&::-webkit-search-cancel-button]:appearance-none",startAdornment:e(f,{position:"start",children:e(w,{"aria-hidden":true})}),endAdornment:I?e(f,{position:"end",children:e(k,{type:"button",variant:"plain",size:"small","aria-label":x,disabled:m,onClick:S,className:"text-muted-foreground hover:text-foreground",children:e(y,{className:"size-4","aria-hidden":true})})}):void 0})})}
|
|
@@ -1,72 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { Blotter, BlotterRow } from '../../domain/blotter/index.js';
|
|
3
|
-
import { Figure } from '../../domain/figure/index.js';
|
|
4
|
-
import { LedgerPanel } from '../../domain/ledger-sheet/index.js';
|
|
5
|
-
import { SectionLabel } from '../../domain/section-label/index.js';
|
|
6
|
-
import { cn } from '../../lib/utils.js';
|
|
7
|
-
/** Value/delta color per tone. `default` keeps the high-contrast foreground;
|
|
8
|
-
* the rest escalate to a semantic theme token that tracks light/dark.
|
|
9
|
-
*
|
|
10
|
-
* All three escalations read the `-h1a` family, not a mix of `-h1a` and
|
|
11
|
-
* `-text`. `--system-*-text` is the ink of a tinted PILL, measured against its
|
|
12
|
-
* own surface; this card paints on the panel, and `-h1a` is the family gated
|
|
13
|
-
* against `--card` and `--background` in `__tests__/tokens-contract.test.ts`.
|
|
14
|
-
* The two differ by one palette step in dark, so a mixed table gave the three
|
|
15
|
-
* deltas three different weights on the same surface for no reason a reader
|
|
16
|
-
* could see. */
|
|
17
|
-
const TONE_VALUE = {
|
|
18
|
-
default: 'text-foreground',
|
|
19
|
-
success: 'text-system-success-h1a',
|
|
20
|
-
warning: 'text-system-alert-h1a',
|
|
21
|
-
destructive: 'text-system-error-h1a'
|
|
22
|
-
};
|
|
23
|
-
/** Trend stroke color per tone. Resolved through the `--color-*` wrappers (the
|
|
24
|
-
* raw `--chart-N` tokens are bare HSL triples, not usable colors). */
|
|
25
|
-
const TONE_TREND = {
|
|
26
|
-
default: 'var(--color-chart-1)',
|
|
27
|
-
success: 'var(--color-system-success)',
|
|
28
|
-
warning: 'var(--color-system-alert)',
|
|
29
|
-
destructive: 'var(--color-destructive)'
|
|
30
|
-
};
|
|
31
|
-
const TREND_VIEWBOX_WIDTH = 100;
|
|
32
|
-
const TREND_VIEWBOX_HEIGHT = 40;
|
|
33
|
-
/**
|
|
34
|
-
* Chrome-less inline trend: no axes, grid, legend or tooltip. Decorative by
|
|
35
|
-
* construction (`aria-hidden`) — the figure above it carries the meaning.
|
|
36
|
-
* Renders nothing when fewer than two points are plottable.
|
|
37
|
-
*/
|
|
38
|
-
function TrendSpark({ data, dataKey, color }) {
|
|
39
|
-
// Drop absent samples BEFORE coercing: Number(null) and Number('') are both
|
|
40
|
-
// 0, which would draw a phantom dip to the floor where the series simply has
|
|
41
|
-
// no reading.
|
|
42
|
-
const points = [];
|
|
43
|
-
for (const row of data) {
|
|
44
|
-
const raw = row[dataKey];
|
|
45
|
-
if (raw === null || raw === undefined || raw === '')
|
|
46
|
-
continue;
|
|
47
|
-
const n = Number(raw);
|
|
48
|
-
if (Number.isFinite(n))
|
|
49
|
-
points.push(n);
|
|
50
|
-
}
|
|
51
|
-
if (points.length < 2)
|
|
52
|
-
return null;
|
|
53
|
-
const min = Math.min(...points);
|
|
54
|
-
const max = Math.max(...points);
|
|
55
|
-
// A flat series has zero span; pin it to the vertical midline instead of
|
|
56
|
-
// dividing by zero.
|
|
57
|
-
const span = max - min;
|
|
58
|
-
const stepX = TREND_VIEWBOX_WIDTH / (points.length - 1);
|
|
59
|
-
const coords = points.map((n, i) => {
|
|
60
|
-
const x = i * stepX;
|
|
61
|
-
const y = span === 0
|
|
62
|
-
? TREND_VIEWBOX_HEIGHT / 2
|
|
63
|
-
: TREND_VIEWBOX_HEIGHT - ((n - min) / span) * TREND_VIEWBOX_HEIGHT;
|
|
64
|
-
return `${x.toFixed(2)},${y.toFixed(2)}`;
|
|
65
|
-
});
|
|
66
|
-
return (_jsxs("svg", { "aria-hidden": true, role: "presentation", viewBox: `0 0 ${TREND_VIEWBOX_WIDTH} ${TREND_VIEWBOX_HEIGHT}`, preserveAspectRatio: "none", className: "h-10 w-full", children: [_jsx("polygon", { points: `0,${TREND_VIEWBOX_HEIGHT} ${coords.join(' ')} ${TREND_VIEWBOX_WIDTH},${TREND_VIEWBOX_HEIGHT}`, fill: color, fillOpacity: 0.15 }), _jsx("polyline", { points: coords.join(' '), fill: "none", stroke: color, strokeWidth: 1.75, vectorEffect: "non-scaling-stroke" })] }));
|
|
67
|
-
}
|
|
68
|
-
export function StatCard({ label, value, delta, tone = 'default', trend, trendKey = 'value', rows, className }) {
|
|
69
|
-
const hasTrend = trend !== undefined && trend.length > 0;
|
|
70
|
-
const hasRows = rows !== undefined && rows.length > 0;
|
|
71
|
-
return (_jsxs(LedgerPanel, { "data-tone": tone, className: className, children: [_jsx(SectionLabel, { children: label }), _jsxs("div", { className: "space-y-1", children: [_jsx(Figure, { size: "hero", className: cn('block', TONE_VALUE[tone]), children: value }), delta !== undefined ? (_jsx("p", { className: cn('text-xs tabular-nums', TONE_VALUE[tone]), children: delta })) : null] }), hasTrend ? (_jsx(TrendSpark, { data: trend, dataKey: trendKey, color: TONE_TREND[tone] })) : null, hasRows ? (_jsx(Blotter, { className: "mt-auto", children: rows.map((row, i) => (_jsx(BlotterRow, { label: row.label, value: row.value }, i))) })) : null] }));
|
|
72
|
-
}
|
|
1
|
+
import{jsx as s,jsxs as h}from"react/jsx-runtime";import{Blotter as g,BlotterRow as y}from"../../domain/blotter/index.js";import{Figure as E}from"../../domain/figure/index.js";import{LedgerPanel as T}from"../../domain/ledger-sheet/index.js";import{SectionLabel as _}from"../../domain/section-label/index.js";import{cn as x}from"../../lib/utils.js";const v={default:"text-foreground",success:"text-system-success-h1a",warning:"text-system-alert-h1a",destructive:"text-system-error-h1a"};const $={default:"var(--color-chart-1)",success:"var(--color-system-success)",warning:"var(--color-system-alert)",destructive:"var(--color-destructive)"};const p=100;const a=40;function b({data:u,dataKey:d,color:l}){const e=[];for(const c of u){const t=c[d];if(t===null||t===void 0||t==="")continue;const r=Number(t);if(Number.isFinite(r))e.push(r)}if(e.length<2)return null;const n=Math.min(...e);const m=Math.max(...e);const o=m-n;const f=p/(e.length-1);const i=e.map((c,t)=>{const r=t*f;const N=o===0?a/2:a-(c-n)/o*a;return`${r.toFixed(2)},${N.toFixed(2)}`});return h("svg",{"aria-hidden":true,role:"presentation",viewBox:`0 0 ${p} ${a}`,preserveAspectRatio:"none",className:"h-10 w-full",children:[s("polygon",{points:`0,${a} ${i.join(" ")} ${p},${a}`,fill:l,fillOpacity:.15}),s("polyline",{points:i.join(" "),fill:"none",stroke:l,strokeWidth:1.75,vectorEffect:"non-scaling-stroke"})]})}export function StatCard({label:u,value:d,delta:l,tone:e="default",trend:n,trendKey:m="value",rows:o,className:f}){const i=n!==void 0&&n.length>0;const c=o!==void 0&&o.length>0;return h(T,{"data-tone":e,className:f,children:[s(_,{children:u}),h("div",{className:"space-y-1",children:[s(E,{size:"hero",className:x("block",v[e]),children:d}),l!==void 0?s("p",{className:x("text-xs tabular-nums",v[e]),children:l}):null]}),i?s(b,{data:n,dataKey:m,color:$[e]}):null,c?s(g,{className:"mt-auto",children:o.map((t,r)=>s(y,{label:t.label,value:t.value},r))}):null]})}
|
|
@@ -1,116 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
/**
|
|
3
|
-
* StatusBadge maps a status/severity string to a Badge variant and a humanized
|
|
4
|
-
* label. Unknown values fall back to a neutral "outline" badge with a
|
|
5
|
-
* title-cased label, so new server enum values never render raw or crash.
|
|
6
|
-
*
|
|
7
|
-
* The variant map is injectable (`variantMap`) and shallow-merges over a
|
|
8
|
-
* generic lifecycle default, so apps extend it with their own domain statuses
|
|
9
|
-
* without forking the component.
|
|
10
|
-
*
|
|
11
|
-
* INVARIANT (WCAG 1.4.1): the bare badge is color-only — the variant tint is the
|
|
12
|
-
* sole carrier of severity. That is a non-color-cue failure for a severity badge
|
|
13
|
-
* (a CRITICAL risk band must read as critical in grayscale and to a color-blind
|
|
14
|
-
* reader). `withIcon` opts the badge into the non-color canon: a leading
|
|
15
|
-
* severity GLYPH plus an sr-only severity word, with the tint as reinforcement
|
|
16
|
-
* only. It is opt-in so existing color-only usage is unaffected.
|
|
17
|
-
*
|
|
18
|
-
* Pure display: no directive, no interactivity. A leading lucide glyph is
|
|
19
|
-
* server-safe, so the component stays RSC-safe for the consuming Next.js app.
|
|
20
|
-
*/
|
|
21
|
-
import { Check, Minus, OctagonX, TriangleAlert } from 'lucide-react';
|
|
22
|
-
import { Badge } from '../../components/ui/badge/index.js';
|
|
23
|
-
import { cn } from '../../lib/utils.js';
|
|
24
|
-
/**
|
|
25
|
-
* Generic lifecycle defaults — extend per-app via the `variantMap` prop.
|
|
26
|
-
*
|
|
27
|
-
* The KEYS are the frozen legacy contract (apps merge their own domain statuses
|
|
28
|
-
* into this map). The VALUES are sindarian-ui Badge variants: sindarian-ui is
|
|
29
|
-
* senior, so the legacy `muted`/`warning` variants resolve to sindarian-ui's
|
|
30
|
-
* `inactive`/`alert`.
|
|
31
|
-
*/
|
|
32
|
-
export const DEFAULT_STATUS_VARIANTS = {
|
|
33
|
-
DRAFT: 'inactive',
|
|
34
|
-
ACTIVE: 'success',
|
|
35
|
-
ENABLED: 'success',
|
|
36
|
-
INACTIVE: 'inactive',
|
|
37
|
-
DISABLED: 'inactive',
|
|
38
|
-
PAUSED: 'alert',
|
|
39
|
-
ARCHIVED: 'inactive',
|
|
40
|
-
QUEUED: 'secondary',
|
|
41
|
-
PENDING: 'alert',
|
|
42
|
-
PROCESSING: 'alert',
|
|
43
|
-
RUNNING: 'alert',
|
|
44
|
-
COMPLETE: 'success',
|
|
45
|
-
COMPLETED: 'success',
|
|
46
|
-
SUCCEEDED: 'success',
|
|
47
|
-
SUCCESS: 'success',
|
|
48
|
-
FAILED: 'destructive',
|
|
49
|
-
ERROR: 'destructive',
|
|
50
|
-
CANCELED: 'inactive',
|
|
51
|
-
CANCELLED: 'inactive',
|
|
52
|
-
EXPIRED: 'inactive',
|
|
53
|
-
OPEN: 'alert',
|
|
54
|
-
RESOLVED: 'success',
|
|
55
|
-
REJECTED: 'destructive',
|
|
56
|
-
CONFIRMED: 'success',
|
|
57
|
-
LOW: 'inactive',
|
|
58
|
-
MEDIUM: 'secondary',
|
|
59
|
-
HIGH: 'alert',
|
|
60
|
-
CRITICAL: 'destructive'
|
|
61
|
-
};
|
|
62
|
-
/**
|
|
63
|
-
* Variant → non-color severity cue: a distinct glyph + an sr-only severity word.
|
|
64
|
-
* This is the load-bearing cue when `withIcon` is set; the Badge's own tint only
|
|
65
|
-
* reinforces it, so severity survives grayscale and color-vision deficiency.
|
|
66
|
-
*
|
|
67
|
-
* A constant lookup keyed on the Badge variant — the variant already encodes the
|
|
68
|
-
* severity, so the glyph follows the variant rather than re-deriving severity
|
|
69
|
-
* from the raw status. `secondary` is a neutral bucket (e.g. MEDIUM/QUEUED) and,
|
|
70
|
-
* like `inactive`, carries the calm dash glyph. The pure color-carrier variants
|
|
71
|
-
* and the unknown `outline` fallback have no severity semantics, so they render
|
|
72
|
-
* no glyph.
|
|
73
|
-
*/
|
|
74
|
-
const SEVERITY_CUE = {
|
|
75
|
-
success: { Icon: Check, word: 'OK' },
|
|
76
|
-
alert: { Icon: TriangleAlert, word: 'Warning' },
|
|
77
|
-
destructive: { Icon: OctagonX, word: 'Critical' },
|
|
78
|
-
inactive: { Icon: Minus, word: 'Neutral' },
|
|
79
|
-
secondary: { Icon: Minus, word: 'Neutral' }
|
|
80
|
-
};
|
|
81
|
-
/**
|
|
82
|
-
* Merge a caller's map over the defaults, upper-casing its KEYS.
|
|
83
|
-
*
|
|
84
|
-
* Lookup has always upper-cased the incoming `status`, but sindarian-x merged
|
|
85
|
-
* `variantMap` verbatim — so `{ reconciled: 'success' }` could never match and
|
|
86
|
-
* fell through to the neutral `outline` badge, silently. That was an unstated
|
|
87
|
-
* requirement rather than a design choice: lender's own call sites work around
|
|
88
|
-
* it by hand (`variantMap={{ [reason.toUpperCase()]: 'destructive' }}`).
|
|
89
|
-
* Normalising here is strictly additive — already-uppercase keys are unchanged.
|
|
90
|
-
*/
|
|
91
|
-
function mergeVariants(variantMap) {
|
|
92
|
-
if (!variantMap)
|
|
93
|
-
return DEFAULT_STATUS_VARIANTS;
|
|
94
|
-
const merged = { ...DEFAULT_STATUS_VARIANTS };
|
|
95
|
-
for (const [key, variant] of Object.entries(variantMap)) {
|
|
96
|
-
merged[key.toUpperCase()] = variant;
|
|
97
|
-
}
|
|
98
|
-
return merged;
|
|
99
|
-
}
|
|
100
|
-
function humanize(value) {
|
|
101
|
-
return value
|
|
102
|
-
.toLowerCase()
|
|
103
|
-
.split(/[_\s]+/)
|
|
104
|
-
.map((w) => (w[0] ? w[0].toUpperCase() + w.slice(1) : w))
|
|
105
|
-
.join(' ');
|
|
106
|
-
}
|
|
107
|
-
export function StatusBadge({ status, label, variantMap, withIcon = false, unknownLabel = 'Unknown', className }) {
|
|
108
|
-
if (!status) {
|
|
109
|
-
return (_jsx(Badge, { variant: "outline", className: cn('text-muted-foreground', className), children: label ?? unknownLabel }));
|
|
110
|
-
}
|
|
111
|
-
const variant = mergeVariants(variantMap)[status.toUpperCase()] ?? 'outline';
|
|
112
|
-
// The glyph follows the resolved variant; an unknown/non-severity variant has
|
|
113
|
-
// no cue, so the badge gracefully renders label-only even with withIcon set.
|
|
114
|
-
const cue = withIcon ? SEVERITY_CUE[variant] : undefined;
|
|
115
|
-
return (_jsxs(Badge, { variant: variant, className: className, children: [cue ? (_jsxs(_Fragment, { children: [_jsx(cue.Icon, { "aria-hidden": true, className: "size-3.5 shrink-0" }), _jsxs("span", { className: "sr-only", children: [cue.word, ": "] })] })) : null, label ?? humanize(status)] }));
|
|
116
|
-
}
|
|
1
|
+
import{jsx as o,jsxs as c,Fragment as l}from"react/jsx-runtime";import{Check as C,Minus as E,OctagonX as I,TriangleAlert as D}from"lucide-react";import{Badge as u}from"../../components/ui/badge/index.js";import{cn as m}from"../../lib/utils.js";export const DEFAULT_STATUS_VARIANTS={DRAFT:"inactive",ACTIVE:"success",ENABLED:"success",INACTIVE:"inactive",DISABLED:"inactive",PAUSED:"alert",ARCHIVED:"inactive",QUEUED:"secondary",PENDING:"alert",PROCESSING:"alert",RUNNING:"alert",COMPLETE:"success",COMPLETED:"success",SUCCEEDED:"success",SUCCESS:"success",FAILED:"destructive",ERROR:"destructive",CANCELED:"inactive",CANCELLED:"inactive",EXPIRED:"inactive",OPEN:"alert",RESOLVED:"success",REJECTED:"destructive",CONFIRMED:"success",LOW:"inactive",MEDIUM:"secondary",HIGH:"alert",CRITICAL:"destructive"};const N={success:{Icon:C,word:"OK"},alert:{Icon:D,word:"Warning"},destructive:{Icon:I,word:"Critical"},inactive:{Icon:E,word:"Neutral"},secondary:{Icon:E,word:"Neutral"}};function v(r){if(!r)return DEFAULT_STATUS_VARIANTS;const e={...DEFAULT_STATUS_VARIANTS};for(const[s,t]of Object.entries(r)){e[s.toUpperCase()]=t}return e}function A(r){return r.toLowerCase().split(/[_\s]+/).map(e=>e[0]?e[0].toUpperCase()+e.slice(1):e).join(" ")}export function StatusBadge({status:r,label:e,variantMap:s,withIcon:t=false,unknownLabel:d="Unknown",className:i}){if(!r){return o(u,{variant:"outline",className:m("text-muted-foreground",i),children:e??d})}const a=v(s)[r.toUpperCase()]??"outline";const n=t?N[a]:void 0;return c(u,{variant:a,className:i,children:[n?c(l,{children:[o(n.Icon,{"aria-hidden":true,className:"size-3.5 shrink-0"}),c("span",{className:"sr-only",children:[n.word,": "]})]}):null,e??A(r)]})}
|
|
@@ -1,31 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import * as React from 'react';
|
|
3
|
-
/** The one breakpoint the hook subscribes at. Exported so its tests can bind to
|
|
4
|
-
* it instead of re-stating the number. */
|
|
5
|
-
export const MOBILE_BREAKPOINT = 768;
|
|
6
|
-
/**
|
|
7
|
-
* True while the viewport is narrower than the 768px mobile breakpoint.
|
|
8
|
-
*
|
|
9
|
-
* Always false on the FIRST render, server and client alike, so a narrow
|
|
10
|
-
* viewport can never produce a hydration mismatch. The correction lands in a
|
|
11
|
-
* `useEffect`, which runs AFTER the browser paints: on a narrow viewport the
|
|
12
|
-
* first painted frame therefore shows the desktop branch and the mobile branch
|
|
13
|
-
* appears on the next frame. That one frame is the deliberate price of
|
|
14
|
-
* hydration safety — a layout effect would remove it but would also change the
|
|
15
|
-
* render timing every existing consumer is built on.
|
|
16
|
-
*
|
|
17
|
-
* Every reading comes from the media query itself rather than a mix of
|
|
18
|
-
* `matchMedia` and `window.innerWidth`, so the initial value and the change
|
|
19
|
-
* events can never disagree at the breakpoint boundary.
|
|
20
|
-
*/
|
|
21
|
-
export function useIsMobile() {
|
|
22
|
-
const [isMobile, setIsMobile] = React.useState(false);
|
|
23
|
-
React.useEffect(() => {
|
|
24
|
-
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
|
|
25
|
-
const onChange = () => setIsMobile(mql.matches);
|
|
26
|
-
onChange();
|
|
27
|
-
mql.addEventListener('change', onChange);
|
|
28
|
-
return () => mql.removeEventListener('change', onChange);
|
|
29
|
-
}, []);
|
|
30
|
-
return isMobile;
|
|
31
|
-
}
|
|
1
|
+
"use client";import*as n from"react";export const MOBILE_BREAKPOINT=768;export function useIsMobile(){const[s,o]=n.useState(false);n.useEffect(()=>{const e=window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT-1}px)`);const t=()=>o(e.matches);t();e.addEventListener("change",t);return()=>e.removeEventListener("change",t)},[]);return s}
|