@svgrid/enterprise 3.1.7 → 3.1.9
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/README.md +1 -0
- package/dist/SvAlertRuleEditor.svelte +16 -5
- package/dist/SvGridAlerts.svelte +8 -0
- package/dist/SvGridAlerts.svelte.d.ts +4 -0
- package/dist/SvGridBoard.svelte +69 -10
- package/dist/SvGridBulkEditDrawer.svelte +13 -5
- package/dist/SvGridGallery.svelte +286 -0
- package/dist/SvGridGallery.svelte.d.ts +30 -0
- package/dist/SvGridScheduler.svelte +33 -11
- package/dist/SvGridSelectionBar.svelte +47 -0
- package/dist/advanced-filter/expr-columns-from-grid.d.ts +1 -0
- package/dist/advanced-filter/expr-columns-from-grid.js +11 -7
- package/dist/advanced-filter-enable.d.ts +6 -0
- package/dist/advanced-filter-enable.js +8 -0
- package/dist/alerts/alert-engine.d.ts +7 -0
- package/dist/alerts/alert-engine.js +5 -2
- package/dist/alerts/condition-editor.d.ts +26 -0
- package/dist/alerts/condition-editor.js +17 -0
- package/dist/base/index.d.ts +20 -0
- package/dist/base/index.js +20 -0
- package/dist/bulk-edit.d.ts +11 -0
- package/dist/bulk-edit.js +18 -1
- package/dist/cdn/svgrid-enterprise.svelte-external.js +11375 -10805
- package/dist/designer/assets/GridFooter-Cu8_yBlQ.js +2 -0
- package/dist/designer/assets/GridMenus-BUjkkAU2.js +6 -0
- package/dist/designer/assets/PinSplitter-BgTpgh1d.css +1 -0
- package/dist/designer/assets/PinSplitter-C5AwA1xV.js +1 -0
- package/dist/designer/assets/SvGrid.helpers-CIrmIo9U.js +1 -0
- package/dist/designer/assets/SvGridCellEditor-CJxThKL_.js +4 -0
- package/dist/designer/assets/SvListBox-CS7oUAiX.js +2 -0
- package/dist/designer/assets/conditional-formatting-MDWbF8_Y.js +1 -0
- package/dist/designer/assets/grid-extension.svelte-BZ7AqyJI.js +1 -0
- package/dist/designer/assets/{index-DtBIaQno.js → index-BPU2KNc5.js} +396 -395
- package/dist/designer/assets/index-BmvG99kR.css +1 -0
- package/dist/designer/assets/sparkline-C-NcnB2i.js +1 -0
- package/dist/designer/assets/src-C_U1NKOY.js +58 -0
- package/dist/designer/assets/sv-grid-scrollbar-B9N6Wuj0.js +109 -0
- package/dist/designer/assets/validate-H5u2HEQh.js +1 -0
- package/dist/designer/index.html +11 -7
- package/dist/expressions/compile.js +10 -0
- package/dist/expressions/evaluate.js +18 -0
- package/dist/expressions/expression-types.d.ts +10 -0
- package/dist/expressions/kinds.d.ts +28 -0
- package/dist/expressions/kinds.js +19 -0
- package/dist/expressions/parse.js +9 -1
- package/dist/fields/AttachmentCell.svelte +242 -0
- package/dist/fields/AttachmentCell.svelte.d.ts +5 -0
- package/dist/fields/AttachmentDialog.svelte +357 -0
- package/dist/fields/AttachmentDialog.svelte.d.ts +13 -0
- package/dist/fields/AttachmentEditor.svelte +41 -0
- package/dist/fields/AttachmentEditor.svelte.d.ts +11 -0
- package/dist/fields/AttachmentPreview.svelte +72 -0
- package/dist/fields/AttachmentPreview.svelte.d.ts +8 -0
- package/dist/fields/BarcodeCell.svelte +20 -0
- package/dist/fields/BarcodeCell.svelte.d.ts +5 -0
- package/dist/fields/BaseAutomations.svelte +558 -0
- package/dist/fields/BaseAutomations.svelte.d.ts +18 -0
- package/dist/fields/BaseChartView.svelte +47 -0
- package/dist/fields/BaseChartView.svelte.d.ts +8 -0
- package/dist/fields/BaseTableView.svelte +185 -0
- package/dist/fields/BaseTableView.svelte.d.ts +37 -0
- package/dist/fields/BaseTabs.svelte +308 -0
- package/dist/fields/BaseTabs.svelte.d.ts +21 -0
- package/dist/fields/ButtonCell.svelte +47 -0
- package/dist/fields/ButtonCell.svelte.d.ts +20 -0
- package/dist/fields/CheckboxCell.svelte +46 -0
- package/dist/fields/CheckboxCell.svelte.d.ts +14 -0
- package/dist/fields/DeleteFieldConfirm.svelte +80 -0
- package/dist/fields/DeleteFieldConfirm.svelte.d.ts +8 -0
- package/dist/fields/DeleteRecordConfirm.svelte +48 -0
- package/dist/fields/DeleteRecordConfirm.svelte.d.ts +9 -0
- package/dist/fields/FieldCell.svelte +80 -0
- package/dist/fields/FieldCell.svelte.d.ts +9 -0
- package/dist/fields/FieldEditor.svelte +321 -0
- package/dist/fields/FieldEditor.svelte.d.ts +10 -0
- package/dist/fields/FieldInput.svelte +188 -0
- package/dist/fields/FieldInput.svelte.d.ts +29 -0
- package/dist/fields/FieldsHeaderEnd.svelte +146 -0
- package/dist/fields/FieldsHeaderEnd.svelte.d.ts +6 -0
- package/dist/fields/FootRowAdder.svelte +8 -0
- package/dist/fields/FootRowAdder.svelte.d.ts +6 -0
- package/dist/fields/GroupHeader.svelte +127 -0
- package/dist/fields/GroupHeader.svelte.d.ts +24 -0
- package/dist/fields/LinkCell.svelte +46 -0
- package/dist/fields/LinkCell.svelte.d.ts +9 -0
- package/dist/fields/LinkPickerEditor.svelte +172 -0
- package/dist/fields/LinkPickerEditor.svelte.d.ts +9 -0
- package/dist/fields/LinkPreview.svelte +60 -0
- package/dist/fields/LinkPreview.svelte.d.ts +7 -0
- package/dist/fields/LinkedRecordView.svelte +94 -0
- package/dist/fields/LinkedRecordView.svelte.d.ts +9 -0
- package/dist/fields/LinkedRecordsCell.svelte +59 -0
- package/dist/fields/LinkedRecordsCell.svelte.d.ts +11 -0
- package/dist/fields/LongTextEditor.svelte +157 -0
- package/dist/fields/LongTextEditor.svelte.d.ts +11 -0
- package/dist/fields/NumberEditor.svelte +309 -0
- package/dist/fields/NumberEditor.svelte.d.ts +12 -0
- package/dist/fields/OptionsEditor.svelte +117 -0
- package/dist/fields/OptionsEditor.svelte.d.ts +11 -0
- package/dist/fields/PercentCell.svelte +21 -0
- package/dist/fields/PercentCell.svelte.d.ts +8 -0
- package/dist/fields/PropertyInput.svelte +204 -0
- package/dist/fields/PropertyInput.svelte.d.ts +11 -0
- package/dist/fields/RatingCell.svelte +75 -0
- package/dist/fields/RatingCell.svelte.d.ts +16 -0
- package/dist/fields/RatingEditor.svelte +82 -0
- package/dist/fields/RatingEditor.svelte.d.ts +9 -0
- package/dist/fields/RecordView.svelte +310 -0
- package/dist/fields/RecordView.svelte.d.ts +11 -0
- package/dist/fields/RichTextCell.svelte +30 -0
- package/dist/fields/RichTextCell.svelte.d.ts +5 -0
- package/dist/fields/RowAdder.svelte +93 -0
- package/dist/fields/RowAdder.svelte.d.ts +7 -0
- package/dist/fields/RowHeader.svelte +99 -0
- package/dist/fields/RowHeader.svelte.d.ts +11 -0
- package/dist/fields/SelectCell.svelte +35 -0
- package/dist/fields/SelectCell.svelte.d.ts +15 -0
- package/dist/fields/SelectEditor.svelte +350 -0
- package/dist/fields/SelectEditor.svelte.d.ts +17 -0
- package/dist/fields/SummaryCell.svelte +160 -0
- package/dist/fields/SummaryCell.svelte.d.ts +11 -0
- package/dist/fields/SvBase.svelte +545 -0
- package/dist/fields/SvBase.svelte.d.ts +30 -0
- package/dist/fields/SvBaseForm.svelte +295 -0
- package/dist/fields/SvBaseForm.svelte.d.ts +22 -0
- package/dist/fields/SvBaseViewBar.svelte +644 -0
- package/dist/fields/SvBaseViewBar.svelte.d.ts +26 -0
- package/dist/fields/UserCell.svelte +39 -0
- package/dist/fields/UserCell.svelte.d.ts +9 -0
- package/dist/fields/UserFace.svelte +30 -0
- package/dist/fields/UserFace.svelte.d.ts +12 -0
- package/dist/fields/activity.svelte.d.ts +50 -0
- package/dist/fields/activity.svelte.js +110 -0
- package/dist/fields/attachments.d.ts +95 -0
- package/dist/fields/attachments.js +145 -0
- package/dist/fields/automation-outbound.d.ts +46 -0
- package/dist/fields/automation-outbound.js +61 -0
- package/dist/fields/automation-server.d.ts +53 -0
- package/dist/fields/automation-server.js +333 -0
- package/dist/fields/automations.svelte.d.ts +208 -0
- package/dist/fields/automations.svelte.js +437 -0
- package/dist/fields/bar-request.svelte.d.ts +16 -0
- package/dist/fields/bar-request.svelte.js +15 -0
- package/dist/fields/base-alerts.d.ts +24 -0
- package/dist/fields/base-alerts.js +64 -0
- package/dist/fields/base-connect.d.ts +82 -0
- package/dist/fields/base-connect.js +271 -0
- package/dist/fields/base-schema.d.ts +104 -0
- package/dist/fields/base-schema.js +266 -0
- package/dist/fields/base-sql.d.ts +62 -0
- package/dist/fields/base-sql.js +251 -0
- package/dist/fields/base-sync.d.ts +142 -0
- package/dist/fields/base-sync.js +499 -0
- package/dist/fields/base-tables.d.ts +89 -0
- package/dist/fields/base-tables.js +349 -0
- package/dist/fields/builtin-types.d.ts +13 -0
- package/dist/fields/builtin-types.js +773 -0
- package/dist/fields/button.d.ts +2 -0
- package/dist/fields/button.js +17 -0
- package/dist/fields/chart-view.d.ts +35 -0
- package/dist/fields/chart-view.js +154 -0
- package/dist/fields/column-order.d.ts +6 -0
- package/dist/fields/column-order.js +12 -0
- package/dist/fields/convert.d.ts +16 -0
- package/dist/fields/convert.js +176 -0
- package/dist/fields/copy-text.d.ts +13 -0
- package/dist/fields/copy-text.js +45 -0
- package/dist/fields/editor-request.svelte.d.ts +71 -0
- package/dist/fields/editor-request.svelte.js +52 -0
- package/dist/fields/field-ops.d.ts +72 -0
- package/dist/fields/field-ops.js +334 -0
- package/dist/fields/filter-model.d.ts +84 -0
- package/dist/fields/filter-model.js +446 -0
- package/dist/fields/format.d.ts +109 -0
- package/dist/fields/format.js +288 -0
- package/dist/fields/formula.d.ts +50 -0
- package/dist/fields/formula.js +530 -0
- package/dist/fields/grid-access.d.ts +60 -0
- package/dist/fields/grid-access.js +120 -0
- package/dist/fields/index.d.ts +34 -0
- package/dist/fields/index.js +312 -0
- package/dist/fields/linked-fields.d.ts +8 -0
- package/dist/fields/linked-fields.js +40 -0
- package/dist/fields/links.d.ts +10 -0
- package/dist/fields/links.js +46 -0
- package/dist/fields/name-list.d.ts +16 -0
- package/dist/fields/name-list.js +49 -0
- package/dist/fields/palette.d.ts +47 -0
- package/dist/fields/palette.js +69 -0
- package/dist/fields/record-delete.svelte.d.ts +12 -0
- package/dist/fields/record-delete.svelte.js +5 -0
- package/dist/fields/record-menu.d.ts +9 -0
- package/dist/fields/record-menu.js +78 -0
- package/dist/fields/registry.svelte.d.ts +176 -0
- package/dist/fields/registry.svelte.js +51 -0
- package/dist/fields/rich-text.d.ts +23 -0
- package/dist/fields/rich-text.js +48 -0
- package/dist/fields/row-color-choice.svelte.d.ts +31 -0
- package/dist/fields/row-color-choice.svelte.js +25 -0
- package/dist/fields/row-colors.d.ts +36 -0
- package/dist/fields/row-colors.js +90 -0
- package/dist/fields/row-height.d.ts +28 -0
- package/dist/fields/row-height.js +50 -0
- package/dist/fields/sources.svelte.d.ts +80 -0
- package/dist/fields/sources.svelte.js +131 -0
- package/dist/fields/summaries.d.ts +18 -0
- package/dist/fields/summaries.js +138 -0
- package/dist/fields/view-model.d.ts +129 -0
- package/dist/fields/view-model.js +492 -0
- package/dist/fields/viewbar/ChartPanel.svelte +61 -0
- package/dist/fields/viewbar/ChartPanel.svelte.d.ts +9 -0
- package/dist/fields/viewbar/ColorPanel.svelte +112 -0
- package/dist/fields/viewbar/ColorPanel.svelte.d.ts +9 -0
- package/dist/fields/viewbar/ConditionValue.svelte +153 -0
- package/dist/fields/viewbar/ConditionValue.svelte.d.ts +15 -0
- package/dist/fields/viewbar/FieldIcon.svelte +18 -0
- package/dist/fields/viewbar/FieldIcon.svelte.d.ts +9 -0
- package/dist/fields/viewbar/FilterPanel.svelte +192 -0
- package/dist/fields/viewbar/FilterPanel.svelte.d.ts +13 -0
- package/dist/fields/viewbar/GanttPanel.svelte +60 -0
- package/dist/fields/viewbar/GanttPanel.svelte.d.ts +10 -0
- package/dist/fields/viewbar/HideFieldsPanel.svelte +104 -0
- package/dist/fields/viewbar/HideFieldsPanel.svelte.d.ts +7 -0
- package/dist/fields/viewbar/SortPanel.svelte +85 -0
- package/dist/fields/viewbar/SortPanel.svelte.d.ts +8 -0
- package/dist/fields/viewbar/ViewsMenu.svelte +250 -0
- package/dist/fields/viewbar/ViewsMenu.svelte.d.ts +15 -0
- package/dist/fields/viewbar/panel-focus.d.ts +3 -0
- package/dist/fields/viewbar/panel-focus.js +22 -0
- package/dist/fields/viewbar/sort-state.d.ts +20 -0
- package/dist/fields/viewbar/sort-state.js +24 -0
- package/dist/gallery.d.ts +7 -0
- package/dist/gallery.js +36 -0
- package/dist/gantt/SvGridGantt.svelte +88 -16
- package/dist/gantt/gantt-config.d.ts +28 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +6 -0
- package/dist/install.js +3 -0
- package/dist/license-core.d.ts +3 -3
- package/dist/license.js +1 -0
- package/dist/node/studio.js +141 -20
- package/dist/scheduler-config.d.ts +11 -0
- package/dist/sources/introspect-supabase.js +4 -1
- package/dist/sources/rest.js +6 -2
- package/dist/studio/emit-project.js +25 -6
- package/dist/studio/sveltekit3.js +13 -7
- package/dist/studio/ui-components.generated.js +94 -2
- package/dist/upgrade-prompt.d.ts +1 -1
- package/dist/version.d.ts +2 -2
- package/dist/version.js +2 -2
- package/dist/wc/GridFooter-f5K0aLe_.js +145 -0
- package/dist/wc/GridMenus-FtSN_8t9.js +1369 -0
- package/dist/wc/PinSplitter-D-qtDhSM.js +132 -0
- package/dist/wc/{SvGrid.helpers-zBkDTryR.js → SvGrid.helpers-B2GEKrxS.js} +84 -36
- package/dist/wc/{SvGridCellEditor-CzUKEbkI.js → SvGridCellEditor-B_cLJrsp.js} +20 -9
- package/dist/wc/conditional-formatting-BXis_oP1.js +189 -0
- package/dist/wc/grid-extension.svelte-Bufq4B4C.js +20 -0
- package/dist/wc/sv-sheet-element.js +7561 -7618
- package/dist/wc/svelte-virtualizer.svelte-ehwOupl-.js +360 -0
- package/dist/wc/{validate-43C03DLP.js → validate-BaMO8UPE.js} +1 -1
- package/package.json +14 -3
- package/dist/designer/assets/GridMenus-CvcASlKv.js +0 -8
- package/dist/designer/assets/SvGrid.helpers-DE5mmORW.js +0 -1
- package/dist/designer/assets/SvGridCellEditor-C7NWcnNC.js +0 -4
- package/dist/designer/assets/SvListBox-BlmmzYcU.js +0 -110
- package/dist/designer/assets/index-KmSEY7Lb.css +0 -1
- package/dist/designer/assets/src-DqHCqori.js +0 -58
- package/dist/designer/assets/validate-B71dE1Cs.js +0 -1
- package/dist/wc/GridMenus-fWbBUiRH.js +0 -1088
- package/dist/wc/svelte-virtualizer.svelte-Dntdh8Ms.js +0 -581
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* The one editor of the number field, in every format (registered as
|
|
4
|
+
* 'fieldNumber'; older currency and percent columns use it too). The box
|
|
5
|
+
* shows the plain number with the field's decimal character, with the
|
|
6
|
+
* currency symbol or percent sign right beside it, aligned as the column
|
|
7
|
+
* aligns its values. A value it cannot take shows why under the cell.
|
|
8
|
+
*
|
|
9
|
+
* It reads what people type the way the field writes it: its own thousands
|
|
10
|
+
* and decimal characters, a pasted symbol or percent sign, "2.5k", "3M",
|
|
11
|
+
* "(5)" for -5. On commit the value is rounded to the field's precision (an
|
|
12
|
+
* integer field to whole numbers); a percent field keeps fractions unless
|
|
13
|
+
* its values are 0 to 100. A value that is not a number, or a negative one
|
|
14
|
+
* where the field allows none, is refused and the box stays open.
|
|
15
|
+
*/
|
|
16
|
+
import { portalToBody } from '@svgrid/grid'
|
|
17
|
+
import { onMount } from 'svelte'
|
|
18
|
+
import { fieldPropsOf } from './registry.svelte'
|
|
19
|
+
import { formatDuration, numberEditText, numberFieldShape, numberFromText, numberKind, parseDuration, parseNumberText, roundTo, toNumber, type DurationFormat, type NumberFieldProps } from './format'
|
|
20
|
+
|
|
21
|
+
let {
|
|
22
|
+
value,
|
|
23
|
+
typed = false,
|
|
24
|
+
columnDef,
|
|
25
|
+
onCommit,
|
|
26
|
+
onCancel,
|
|
27
|
+
onCommitAndMove,
|
|
28
|
+
onCommitAndMoveDown,
|
|
29
|
+
}: {
|
|
30
|
+
value: unknown
|
|
31
|
+
typed?: boolean
|
|
32
|
+
columnDef?: Record<string, unknown>
|
|
33
|
+
onCommit: (v?: unknown) => void
|
|
34
|
+
onCancel: () => void
|
|
35
|
+
onCommitAndMove?: (v: unknown, direction: 1 | -1) => void
|
|
36
|
+
onCommitAndMoveDown?: (v: unknown, up?: boolean) => void
|
|
37
|
+
} = $props()
|
|
38
|
+
|
|
39
|
+
const def = $derived((columnDef ?? {}) as { fieldType?: unknown; fieldProps?: Record<string, unknown>; formatter?: unknown })
|
|
40
|
+
const p = $derived(fieldPropsOf(def) as NumberFieldProps)
|
|
41
|
+
const kind = $derived(numberKind(def.fieldType, p))
|
|
42
|
+
const symbol = $derived(kind === 'currency' ? (p.symbol ?? '$') : '')
|
|
43
|
+
const symbolAfter = $derived(p.symbolPosition === 'after')
|
|
44
|
+
// A percent field stores fractions (0.75) unless its values are 0 to 100;
|
|
45
|
+
// the box always shows percent points (75).
|
|
46
|
+
const scale = $derived(numberFieldShape(kind, p).scale)
|
|
47
|
+
// A duration field: seconds, shown and typed as "1:30" (or "1h 30m").
|
|
48
|
+
const isDuration = $derived(def.fieldType === 'duration')
|
|
49
|
+
const durationFormat = $derived(((p as { durationFormat?: unknown }).durationFormat ?? 'h:mm') as DurationFormat)
|
|
50
|
+
|
|
51
|
+
// svelte-ignore state_referenced_locally
|
|
52
|
+
// Typed on the cell: the value is that text, not the cell's number.
|
|
53
|
+
const seeded = typed
|
|
54
|
+
// svelte-ignore state_referenced_locally
|
|
55
|
+
let text = $state(seeded ? keepNumberChars(String(value ?? '')) : initialText(value))
|
|
56
|
+
|
|
57
|
+
function initialText(v: unknown): string {
|
|
58
|
+
const n = toNumber(v)
|
|
59
|
+
if (n == null) return ''
|
|
60
|
+
return def.fieldType === 'duration' ? formatDuration(n, durationFormat) : numberEditText(n * scale, p)
|
|
61
|
+
}
|
|
62
|
+
/** What a typed key may add: digits, separators, a sign, K / M / B, the symbol; for a duration also : h m s. */
|
|
63
|
+
function keepNumberChars(s: string): string {
|
|
64
|
+
return [...s].filter((c) => /[\d.,\s+\-−()%kmbKMB]/.test(c) || (isDuration && /[:hsHS]/.test(c)) || (!!p.symbol && p.symbol.includes(c))).join('')
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const result = $derived(!text.trim() ? null : isDuration ? parseDuration(text) : numberFromText(text, kind, p))
|
|
68
|
+
const error = $derived(
|
|
69
|
+
text.trim() && result == null
|
|
70
|
+
? isDuration ? 'Not a duration (1:30, 1h 30m)' : 'Not a number'
|
|
71
|
+
: result != null && result < 0 && p.allowNegative === false
|
|
72
|
+
? 'Negative numbers are not allowed'
|
|
73
|
+
: null,
|
|
74
|
+
)
|
|
75
|
+
// ---- the spinner: up / down buttons and the Up / Down keys
|
|
76
|
+
// A duration steps a minute (a second when the format shows seconds).
|
|
77
|
+
const step = $derived(
|
|
78
|
+
Number((p as { step?: unknown }).step) > 0 ? Number((p as { step?: unknown }).step) : isDuration ? (durationFormat === 'h:mm' ? 60 : 1) : 1,
|
|
79
|
+
)
|
|
80
|
+
/** Add `dir` steps (ten with Shift) to the number in the box, in the box's own units. */
|
|
81
|
+
function bump(dir: 1 | -1, big = false) {
|
|
82
|
+
if (isDuration) {
|
|
83
|
+
const secs = text.trim() ? parseDuration(text) : 0
|
|
84
|
+
if (secs == null) return
|
|
85
|
+
text = formatDuration(Math.max(0, secs + dir * step * (big ? 10 : 1)), durationFormat)
|
|
86
|
+
return
|
|
87
|
+
}
|
|
88
|
+
const now = text.trim() ? parseNumberText(text, p) : 0
|
|
89
|
+
if (now == null) return
|
|
90
|
+
let next = roundTo(now + dir * step * (big ? 10 : 1), numberFieldShape(kind, p).places)
|
|
91
|
+
if (next < 0 && p.allowNegative === false) next = 0
|
|
92
|
+
text = numberEditText(next, p)
|
|
93
|
+
}
|
|
94
|
+
// Holding a button repeats, after a pause, as a native spinner does.
|
|
95
|
+
let hold: ReturnType<typeof setTimeout> | null = null
|
|
96
|
+
function press(dir: 1 | -1, e: PointerEvent) {
|
|
97
|
+
// mousedown is cancelled too (on the button): that, not this, keeps the
|
|
98
|
+
// focus in the box, so the keys still go to the editor.
|
|
99
|
+
e.preventDefault()
|
|
100
|
+
input?.focus({ preventScroll: true })
|
|
101
|
+
bump(dir, e.shiftKey)
|
|
102
|
+
stopHold()
|
|
103
|
+
const repeat = () => {
|
|
104
|
+
bump(dir, e.shiftKey)
|
|
105
|
+
hold = setTimeout(repeat, 60)
|
|
106
|
+
}
|
|
107
|
+
hold = setTimeout(repeat, 400)
|
|
108
|
+
}
|
|
109
|
+
function stopHold() {
|
|
110
|
+
if (hold) clearTimeout(hold)
|
|
111
|
+
hold = null
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// The number and its symbol sit together where the column puts values.
|
|
115
|
+
const justify = $derived(
|
|
116
|
+
(def as { align?: string }).align === 'left' ? 'flex-start' : (def as { align?: string }).align === 'center' ? 'center' : 'flex-end',
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
// ---- why a value is refused, under the cell, in the body so the cell
|
|
120
|
+
// cannot clip it
|
|
121
|
+
let box = $state<HTMLElement | null>(null)
|
|
122
|
+
let input = $state<HTMLInputElement | null>(null)
|
|
123
|
+
let errorAt = $state<{ top: number; left: number } | null>(null)
|
|
124
|
+
let shake = $state(false)
|
|
125
|
+
function place() {
|
|
126
|
+
const cell = box?.closest('td') ?? box
|
|
127
|
+
if (!cell) return
|
|
128
|
+
const r = cell.getBoundingClientRect()
|
|
129
|
+
errorAt = { top: r.bottom + 2, left: r.left }
|
|
130
|
+
}
|
|
131
|
+
function takeFocus() {
|
|
132
|
+
if (!input || document.activeElement === input) return
|
|
133
|
+
input.focus({ preventScroll: true })
|
|
134
|
+
if (!seeded) input.select()
|
|
135
|
+
else input.setSelectionRange(text.length, text.length)
|
|
136
|
+
}
|
|
137
|
+
onMount(() => {
|
|
138
|
+
place()
|
|
139
|
+
takeFocus()
|
|
140
|
+
// The double-click that opened the editor can still hand focus back to
|
|
141
|
+
// the grid; take it once more on the next frame.
|
|
142
|
+
const frame = requestAnimationFrame(takeFocus)
|
|
143
|
+
// A press anywhere else ends the edit: a number is kept, text that is
|
|
144
|
+
// not one is dropped rather than leave the cell stuck open. Not on blur:
|
|
145
|
+
// focus also leaves when the window does, and the grid can move it.
|
|
146
|
+
const onDown = (e: PointerEvent) => {
|
|
147
|
+
if (finished || box?.contains(e.target as Node)) return
|
|
148
|
+
if (error) {
|
|
149
|
+
finished = true
|
|
150
|
+
onCancel()
|
|
151
|
+
} else finish('commit')
|
|
152
|
+
}
|
|
153
|
+
window.addEventListener('resize', place)
|
|
154
|
+
window.addEventListener('scroll', place, true)
|
|
155
|
+
document.addEventListener('pointerdown', onDown, true)
|
|
156
|
+
return () => {
|
|
157
|
+
stopHold()
|
|
158
|
+
cancelAnimationFrame(frame)
|
|
159
|
+
window.removeEventListener('resize', place)
|
|
160
|
+
window.removeEventListener('scroll', place, true)
|
|
161
|
+
document.removeEventListener('pointerdown', onDown, true)
|
|
162
|
+
}
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
let finished = false
|
|
166
|
+
function finish(how: 'commit' | 'next' | 'prev' | 'down' | 'up'): boolean {
|
|
167
|
+
if (finished) return true
|
|
168
|
+
if (error) {
|
|
169
|
+
shake = false
|
|
170
|
+
queueMicrotask(() => (shake = true))
|
|
171
|
+
return false
|
|
172
|
+
}
|
|
173
|
+
finished = true
|
|
174
|
+
// Enter moves a row down, as in a text cell; Tab a column across.
|
|
175
|
+
if ((how === 'down' || how === 'up') && onCommitAndMoveDown) onCommitAndMoveDown(result, how === 'up')
|
|
176
|
+
else if ((how === 'next' || how === 'prev') && onCommitAndMove) onCommitAndMove(result, how === 'next' ? 1 : -1)
|
|
177
|
+
else onCommit(result)
|
|
178
|
+
return true
|
|
179
|
+
}
|
|
180
|
+
function onKey(e: KeyboardEvent) {
|
|
181
|
+
if (e.key === 'Enter') finish(e.shiftKey ? 'up' : 'down')
|
|
182
|
+
else if (e.key === 'Tab') finish(e.shiftKey ? 'prev' : 'next')
|
|
183
|
+
else if (e.key === 'ArrowUp' || e.key === 'ArrowDown') bump(e.key === 'ArrowUp' ? 1 : -1, e.shiftKey)
|
|
184
|
+
else if (e.key === 'Escape') {
|
|
185
|
+
finished = true
|
|
186
|
+
onCancel()
|
|
187
|
+
} else {
|
|
188
|
+
// Caret keys move in the box, not between cells.
|
|
189
|
+
e.stopPropagation()
|
|
190
|
+
return
|
|
191
|
+
}
|
|
192
|
+
e.preventDefault()
|
|
193
|
+
e.stopPropagation()
|
|
194
|
+
}
|
|
195
|
+
function onBeforeInput(e: InputEvent) {
|
|
196
|
+
// Typed letters that cannot be part of a number never reach the box;
|
|
197
|
+
// a paste is read as a whole on commit.
|
|
198
|
+
if (e.inputType === 'insertText' && e.data && keepNumberChars(e.data) !== e.data) e.preventDefault()
|
|
199
|
+
}
|
|
200
|
+
const inputValue = (e: Event) => (e.currentTarget as HTMLInputElement).value
|
|
201
|
+
</script>
|
|
202
|
+
|
|
203
|
+
<!-- Presses inside the editor (the spinner, placing the caret) stay here: the
|
|
204
|
+
cell's own handlers would move focus to the grid and end the typing. -->
|
|
205
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
206
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
207
|
+
<span
|
|
208
|
+
class="sv-fne"
|
|
209
|
+
class:is-invalid={!!error}
|
|
210
|
+
class:is-shake={shake}
|
|
211
|
+
style:justify-content={justify}
|
|
212
|
+
bind:this={box}
|
|
213
|
+
onanimationend={() => (shake = false)}
|
|
214
|
+
onpointerdown={(e) => e.stopPropagation()}
|
|
215
|
+
onmousedown={(e) => e.stopPropagation()}
|
|
216
|
+
onclick={(e) => e.stopPropagation()}
|
|
217
|
+
ondblclick={(e) => e.stopPropagation()}
|
|
218
|
+
>
|
|
219
|
+
{#if symbol && !symbolAfter}<span class="sv-fne-affix" aria-hidden="true">{symbol}</span>{/if}
|
|
220
|
+
<!-- The input takes the width of its text (a hidden copy sizes it), so
|
|
221
|
+
the symbol sits right against the number. -->
|
|
222
|
+
<span class="sv-fne-sizer" data-text={text}>
|
|
223
|
+
<input
|
|
224
|
+
bind:this={input}
|
|
225
|
+
class="sv-fne-input"
|
|
226
|
+
size="1"
|
|
227
|
+
inputmode="decimal"
|
|
228
|
+
autocomplete="off"
|
|
229
|
+
spellcheck="false"
|
|
230
|
+
aria-label={isDuration ? `Duration (${durationFormat})` : kind === 'currency' ? `Amount in ${symbol || 'money'}` : kind === 'percent' ? 'Percent' : 'Number'}
|
|
231
|
+
aria-invalid={error ? 'true' : undefined}
|
|
232
|
+
value={text}
|
|
233
|
+
oninput={(e) => (text = inputValue(e))}
|
|
234
|
+
onbeforeinput={onBeforeInput}
|
|
235
|
+
onkeydown={onKey}
|
|
236
|
+
/>
|
|
237
|
+
</span>
|
|
238
|
+
{#if symbol && symbolAfter}<span class="sv-fne-affix" aria-hidden="true">{symbol}</span>{/if}
|
|
239
|
+
{#if kind === 'percent'}<span class="sv-fne-affix" aria-hidden="true">%</span>{/if}
|
|
240
|
+
<!-- Pointer only: Up / Down do the same from the keyboard, and focus stays
|
|
241
|
+
in the box. -->
|
|
242
|
+
<span class="sv-fne-spin">
|
|
243
|
+
<button type="button" tabindex="-1" aria-label={`Increase by ${step}`} onmousedown={(e) => e.preventDefault()} onpointerdown={(e) => press(1, e)} onpointerup={stopHold} onpointerleave={stopHold}>
|
|
244
|
+
<svg viewBox="0 0 10 6" width="8" height="5" aria-hidden="true"><path d="M1 5l4-4 4 4" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" /></svg>
|
|
245
|
+
</button>
|
|
246
|
+
<button type="button" tabindex="-1" aria-label={`Decrease by ${step}`} onmousedown={(e) => e.preventDefault()} onpointerdown={(e) => press(-1, e)} onpointerup={stopHold} onpointerleave={stopHold}>
|
|
247
|
+
<svg viewBox="0 0 10 6" width="8" height="5" aria-hidden="true"><path d="M1 1l4 4 4-4" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" /></svg>
|
|
248
|
+
</button>
|
|
249
|
+
</span>
|
|
250
|
+
</span>
|
|
251
|
+
|
|
252
|
+
{#if errorAt && error}
|
|
253
|
+
<div
|
|
254
|
+
class="sv-fne-error"
|
|
255
|
+
role="alert"
|
|
256
|
+
use:portalToBody
|
|
257
|
+
style:top={`${errorAt.top}px`}
|
|
258
|
+
style:left={`${errorAt.left}px`}
|
|
259
|
+
>
|
|
260
|
+
<svg viewBox="0 0 24 24" width="12" height="12" aria-hidden="true"><path d="M12 3 2 20h20L12 3zM12 10v4M12 17v.5" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" /></svg>
|
|
261
|
+
{error}
|
|
262
|
+
</div>
|
|
263
|
+
{/if}
|
|
264
|
+
|
|
265
|
+
<style>
|
|
266
|
+
/* The whole cell, edge to edge, with the input only as wide as its text,
|
|
267
|
+
so the symbol stays beside the number: an input's default width (about
|
|
268
|
+
20 characters) is wider than a number column. The cell's own gutter
|
|
269
|
+
(--sg-cell-px) on the left, the spinner at the right edge with the
|
|
270
|
+
number right-aligned against it. Accent outline while focused, as the
|
|
271
|
+
built-in editors have; red when invalid. */
|
|
272
|
+
.sv-fne {
|
|
273
|
+
position: absolute; inset: 0; z-index: 1;
|
|
274
|
+
display: flex; align-items: center; gap: 1px; padding: 0 22px 0 var(--sg-cell-px, 7px); box-sizing: border-box; overflow: hidden;
|
|
275
|
+
background: var(--sg-input-bg, var(--sg-bg, #fff));
|
|
276
|
+
}
|
|
277
|
+
.sv-fne:focus-within { box-shadow: inset 0 0 0 2px var(--sg-accent, #2563eb); }
|
|
278
|
+
.sv-fne.is-invalid { box-shadow: inset 0 0 0 2px var(--sg-danger, #dc2626); }
|
|
279
|
+
.sv-fne.is-shake { animation: sv-fne-shake 0.24s; }
|
|
280
|
+
@keyframes sv-fne-shake { 25% { transform: translateX(-3px); } 75% { transform: translateX(3px); } }
|
|
281
|
+
@media (prefers-reduced-motion: reduce) { .sv-fne.is-shake { animation: none; } }
|
|
282
|
+
.sv-fne-affix { flex: none; color: var(--sg-fg, #0f172a); font-variant-numeric: tabular-nums; }
|
|
283
|
+
/* Only the hidden copy of the text (plus a pixel for the caret) sets the
|
|
284
|
+
width; the input lies over it. An input's own minimum width (even at
|
|
285
|
+
size=1) would leave a gap after the symbol. */
|
|
286
|
+
.sv-fne-sizer { position: relative; flex: 0 1 auto; min-width: 1ch; max-width: 100%; height: 100%; display: flex; align-items: center; }
|
|
287
|
+
.sv-fne-sizer::after {
|
|
288
|
+
content: attr(data-text); padding-inline-start: 1px; visibility: hidden; white-space: pre; overflow: hidden;
|
|
289
|
+
font: inherit; font-variant-numeric: tabular-nums;
|
|
290
|
+
}
|
|
291
|
+
.sv-fne-input {
|
|
292
|
+
position: absolute; inset: 0; width: 100%; min-width: 0; height: 100%; padding: 0; margin: 0; border: 0; outline: none;
|
|
293
|
+
background: none; box-shadow: none; color: var(--sg-fg, #0f172a); font: inherit; text-align: end; font-variant-numeric: tabular-nums;
|
|
294
|
+
}
|
|
295
|
+
.sv-fne-spin {
|
|
296
|
+
position: absolute; top: 0; bottom: 0; inset-inline-end: 3px;
|
|
297
|
+
display: flex; flex-direction: column; justify-content: center; gap: 1px;
|
|
298
|
+
}
|
|
299
|
+
.sv-fne-spin button {
|
|
300
|
+
display: flex; align-items: center; justify-content: center; width: 16px; height: 11px; padding: 0; border: 0;
|
|
301
|
+
border-radius: 3px; background: none; color: var(--sg-muted, #64748b); cursor: pointer;
|
|
302
|
+
}
|
|
303
|
+
.sv-fne-spin button:hover { background: var(--sg-row-hover-bg, #f1f5f9); color: var(--sg-fg, #0f172a); }
|
|
304
|
+
.sv-fne-error {
|
|
305
|
+
position: fixed; z-index: 2147483646; display: inline-flex; align-items: center; gap: 6px; padding: 3px 8px;
|
|
306
|
+
border-radius: var(--sg-radius, 6px); background: var(--sg-danger, #dc2626); color: #fff;
|
|
307
|
+
font-size: 12px; white-space: nowrap; pointer-events: none;
|
|
308
|
+
}
|
|
309
|
+
</style>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
type $$ComponentProps = {
|
|
2
|
+
value: unknown;
|
|
3
|
+
typed?: boolean;
|
|
4
|
+
columnDef?: Record<string, unknown>;
|
|
5
|
+
onCommit: (v?: unknown) => void;
|
|
6
|
+
onCancel: () => void;
|
|
7
|
+
onCommitAndMove?: (v: unknown, direction: 1 | -1) => void;
|
|
8
|
+
onCommitAndMoveDown?: (v: unknown, up?: boolean) => void;
|
|
9
|
+
};
|
|
10
|
+
declare const NumberEditor: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
11
|
+
type NumberEditor = ReturnType<typeof NumberEditor>;
|
|
12
|
+
export default NumberEditor;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* The select options list in the field editor: a color and a name per
|
|
4
|
+
* option, reorder with the arrows, remove, add, sort A to Z. A swatch opens
|
|
5
|
+
* the 40-color palette (ten hues, four strengths) under its option.
|
|
6
|
+
*/
|
|
7
|
+
import { OPTION_COLORS, OPTION_CYCLE } from './palette'
|
|
8
|
+
|
|
9
|
+
type Option = { value: string; color?: string }
|
|
10
|
+
let { options, onChange }: { options: Option[]; onChange: (next: Option[]) => void } = $props()
|
|
11
|
+
let paletteFor = $state<number | null>(null)
|
|
12
|
+
|
|
13
|
+
// Kept out of the markup: AGENTS.md keeps `as` casts in <script>.
|
|
14
|
+
const inputValue = (e: Event) => (e.currentTarget as HTMLInputElement).value
|
|
15
|
+
|
|
16
|
+
function set(i: number, patch: Partial<Option>) {
|
|
17
|
+
onChange(options.map((o, j) => (j === i ? { ...o, ...patch } : o)))
|
|
18
|
+
}
|
|
19
|
+
function move(i: number, d: number) {
|
|
20
|
+
const j = i + d
|
|
21
|
+
if (j < 0 || j >= options.length) return
|
|
22
|
+
const next = options.slice()
|
|
23
|
+
;[next[i], next[j]] = [next[j]!, next[i]!]
|
|
24
|
+
onChange(next)
|
|
25
|
+
}
|
|
26
|
+
function add() {
|
|
27
|
+
onChange([...options, { value: '', color: OPTION_CYCLE[options.length % OPTION_CYCLE.length] }])
|
|
28
|
+
queueMicrotask(() => {
|
|
29
|
+
const inputs = document.querySelectorAll<HTMLInputElement>('.sv-fopt-name')
|
|
30
|
+
inputs[inputs.length - 1]?.focus()
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
function sortAZ() {
|
|
34
|
+
onChange(options.slice().sort((a, b) => a.value.localeCompare(b.value, undefined, { sensitivity: 'base', numeric: true })))
|
|
35
|
+
}
|
|
36
|
+
</script>
|
|
37
|
+
|
|
38
|
+
<ul class="sv-fopt" aria-label="Options">
|
|
39
|
+
{#each options as o, i (i)}
|
|
40
|
+
<li class="sv-fopt-row">
|
|
41
|
+
<button
|
|
42
|
+
type="button"
|
|
43
|
+
class="sv-fopt-swatch"
|
|
44
|
+
style:background={o.color ?? '#cccccc'}
|
|
45
|
+
aria-label={`Color of ${o.value || 'option ' + (i + 1)}`}
|
|
46
|
+
aria-expanded={paletteFor === i}
|
|
47
|
+
onclick={() => (paletteFor = paletteFor === i ? null : i)}
|
|
48
|
+
></button>
|
|
49
|
+
<input
|
|
50
|
+
class="sv-fopt-name"
|
|
51
|
+
value={o.value}
|
|
52
|
+
placeholder="Option name"
|
|
53
|
+
aria-label={`Option ${i + 1}`}
|
|
54
|
+
oninput={(e) => set(i, { value: inputValue(e) })}
|
|
55
|
+
/>
|
|
56
|
+
<button type="button" class="sv-fopt-icon" aria-label="Move up" disabled={i === 0} onclick={() => move(i, -1)}>
|
|
57
|
+
<svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true"><path d="M6 15l6-6 6 6" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" /></svg>
|
|
58
|
+
</button>
|
|
59
|
+
<button type="button" class="sv-fopt-icon" aria-label="Move down" disabled={i === options.length - 1} onclick={() => move(i, 1)}>
|
|
60
|
+
<svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true"><path d="M6 9l6 6 6-6" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" /></svg>
|
|
61
|
+
</button>
|
|
62
|
+
<button type="button" class="sv-fopt-icon" aria-label={`Remove ${o.value || 'option'}`} onclick={() => onChange(options.filter((_, j) => j !== i))}>
|
|
63
|
+
<svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true"><path d="M6 6l12 12M18 6 6 18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" /></svg>
|
|
64
|
+
</button>
|
|
65
|
+
</li>
|
|
66
|
+
{#if paletteFor === i}
|
|
67
|
+
<li><div class="sv-fopt-palette" role="radiogroup" aria-label={`Color of ${o.value || 'option ' + (i + 1)}`}>
|
|
68
|
+
{#each OPTION_COLORS as c (c.value)}
|
|
69
|
+
<button
|
|
70
|
+
type="button"
|
|
71
|
+
role="radio"
|
|
72
|
+
aria-checked={o.color === c.value}
|
|
73
|
+
aria-label={c.label}
|
|
74
|
+
title={c.label}
|
|
75
|
+
class:is-on={o.color === c.value}
|
|
76
|
+
style:background={c.value}
|
|
77
|
+
onclick={() => {
|
|
78
|
+
set(i, { color: c.value })
|
|
79
|
+
paletteFor = null
|
|
80
|
+
}}
|
|
81
|
+
></button>
|
|
82
|
+
{/each}
|
|
83
|
+
</div></li>
|
|
84
|
+
{/if}
|
|
85
|
+
{/each}
|
|
86
|
+
</ul>
|
|
87
|
+
<div class="sv-fopt-actions">
|
|
88
|
+
<button type="button" class="sv-fopt-add" onclick={add}>+ Add option</button>
|
|
89
|
+
{#if options.length > 1}<button type="button" class="sv-fopt-add" onclick={sortAZ}>Sort A to Z</button>{/if}
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
<style>
|
|
93
|
+
.sv-fopt { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 6px; max-height: 260px; overflow: auto; }
|
|
94
|
+
.sv-fopt-row { display: flex; align-items: center; gap: 6px; }
|
|
95
|
+
.sv-fopt-swatch { flex: none; width: 18px; height: 18px; border-radius: 999px; border: 2px solid var(--sg-bg, #fff); box-shadow: 0 0 0 1px var(--sg-border, #e2e8f0); cursor: pointer; }
|
|
96
|
+
.sv-fopt-swatch:focus-visible { outline: 2px solid var(--sg-accent, #2563eb); outline-offset: 2px; }
|
|
97
|
+
.sv-fopt-name {
|
|
98
|
+
flex: 1; min-width: 0; height: 30px; padding: 0 8px; font: inherit; font-size: 13px;
|
|
99
|
+
border: 1px solid var(--sg-input-border, var(--sg-border, #e2e8f0)); border-radius: var(--sg-radius, 6px);
|
|
100
|
+
background: var(--sg-input-bg, var(--sg-bg, #fff)); color: var(--sg-fg, #0f172a);
|
|
101
|
+
}
|
|
102
|
+
.sv-fopt-name::placeholder { color: var(--sg-muted, #94a3b8); }
|
|
103
|
+
.sv-fopt-name:focus { outline: none; border-color: var(--sg-accent, #2563eb); box-shadow: 0 0 0 3px color-mix(in srgb, var(--sg-accent, #2563eb) 22%, transparent); }
|
|
104
|
+
.sv-fopt-icon { flex: none; display: inline-flex; padding: 4px; border: 0; border-radius: 4px; background: none; color: var(--sg-muted, #64748b); cursor: pointer; }
|
|
105
|
+
.sv-fopt-icon:disabled { opacity: 0.35; cursor: default; }
|
|
106
|
+
.sv-fopt-icon:not(:disabled):hover { color: var(--sg-fg, #0f172a); }
|
|
107
|
+
.sv-fopt-palette {
|
|
108
|
+
display: grid; grid-template-columns: repeat(10, 20px); gap: 5px; padding: 8px; margin-inline-start: 24px;
|
|
109
|
+
border-radius: var(--sg-radius, 6px); border: 1px solid var(--sg-border, #e2e8f0); background: var(--sg-bg, #fff); width: max-content;
|
|
110
|
+
}
|
|
111
|
+
.sv-fopt-palette button { width: 20px; height: 20px; padding: 0; border-radius: 4px; border: 0; cursor: pointer; }
|
|
112
|
+
.sv-fopt-palette button.is-on { box-shadow: 0 0 0 2px var(--sg-bg, #fff), 0 0 0 4px var(--sg-fg, #0f172a); }
|
|
113
|
+
.sv-fopt-palette button:focus-visible { outline: 2px solid var(--sg-accent, #2563eb); outline-offset: 2px; }
|
|
114
|
+
.sv-fopt-actions { display: flex; gap: 14px; margin-top: 6px; }
|
|
115
|
+
.sv-fopt-add { padding: 4px 0; border: 0; background: none; color: var(--sg-muted, #64748b); font: inherit; font-size: 13px; cursor: pointer; }
|
|
116
|
+
.sv-fopt-add:hover { color: var(--sg-fg, #0f172a); }
|
|
117
|
+
</style>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
type Option = {
|
|
2
|
+
value: string;
|
|
3
|
+
color?: string;
|
|
4
|
+
};
|
|
5
|
+
type $$ComponentProps = {
|
|
6
|
+
options: Option[];
|
|
7
|
+
onChange: (next: Option[]) => void;
|
|
8
|
+
};
|
|
9
|
+
declare const OptionsEditor: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
10
|
+
type OptionsEditor = ReturnType<typeof OptionsEditor>;
|
|
11
|
+
export default OptionsEditor;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/** Percent cells with `showBar`: the number and a bar filled to it (0 to 100%). */
|
|
3
|
+
import type { FieldCellProps } from './registry.svelte'
|
|
4
|
+
import { formatPercent, toNumber, type NumberFormatProps } from './format'
|
|
5
|
+
|
|
6
|
+
let { value, props }: FieldCellProps<NumberFormatProps & { points?: boolean }> = $props()
|
|
7
|
+
const n = $derived(toNumber(value))
|
|
8
|
+
const share = $derived(n == null ? 0 : Math.max(0, Math.min(1, props.points ? n / 100 : n)))
|
|
9
|
+
</script>
|
|
10
|
+
|
|
11
|
+
<span class="sv-fpc">
|
|
12
|
+
<span class="sv-fpc-bar" aria-hidden="true"><span style:width={`${share * 100}%`}></span></span>
|
|
13
|
+
<span class="sv-fpc-text">{n == null ? '' : formatPercent(n, props)}</span>
|
|
14
|
+
</span>
|
|
15
|
+
|
|
16
|
+
<style>
|
|
17
|
+
.sv-fpc { display: flex; align-items: center; gap: 8px; width: 100%; }
|
|
18
|
+
.sv-fpc-bar { flex: 1; min-width: 24px; height: 6px; border-radius: 3px; background: var(--sg-row-hover-bg, #f1f5f9); overflow: hidden; }
|
|
19
|
+
.sv-fpc-bar > span { display: block; height: 100%; border-radius: 3px; background: var(--sg-muted, #64748b); }
|
|
20
|
+
.sv-fpc-text { flex: none; font-variant-numeric: tabular-nums; }
|
|
21
|
+
</style>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Percent cells with `showBar`: the number and a bar filled to it (0 to 100%). */
|
|
2
|
+
import type { FieldCellProps } from './registry.svelte.js';
|
|
3
|
+
import { type NumberFormatProps } from './format.js';
|
|
4
|
+
declare const PercentCell: import("svelte").Component<FieldCellProps<NumberFormatProps & {
|
|
5
|
+
points?: boolean;
|
|
6
|
+
}>, {}, "">;
|
|
7
|
+
type PercentCell = ReturnType<typeof PercentCell>;
|
|
8
|
+
export default PercentCell;
|