@lostgradient/cinder 0.11.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/components.json +1 -1
- package/dist/components/code-block/code-block.css +15 -9
- package/dist/components/code-block/code-block.variables.js +8 -2
- package/dist/components/confirm-dialog/confirm-dialog.css +2 -0
- package/dist/components/confirm-dialog/confirm-dialog.schema.js +10 -1
- package/dist/components/confirm-dialog/confirm-dialog.types.d.ts +7 -0
- package/dist/components/confirm-dialog/index.js +478 -106
- package/dist/components/faceted-filter-bar/faceted-filter-bar.schema.js +5 -1
- package/dist/components/faceted-filter-bar/faceted-filter-bar.types.d.ts +2 -0
- package/dist/components/faceted-filter-bar/index.js +40 -31
- package/dist/components/navigation-bar/index.js +11 -17
- package/dist/components/run-step-timeline/index.js +65 -6
- package/dist/components/run-step-timeline/run-step-timeline.utilities.d.ts +9 -1
- package/dist/index.js +352 -246
- package/dist/server/components/color-field/index.js +2 -2
- package/dist/server/components/confirm-dialog/index.js +7 -2
- package/dist/server/components/faceted-filter-bar/index.js +1 -1
- package/dist/server/components/navigation-bar/index.js +1 -1
- package/dist/server/components/run-step-timeline/index.js +1 -1
- package/dist/server/index.js +7 -7
- package/dist/server/{index.server-5tf22d6e.js → index.server-4wx0qven.js} +3 -3
- package/dist/server/{index.server-894t97kv.js → index.server-7yvpb6z7.js} +4 -4
- package/dist/server/{index.server-btgb3d56.js → index.server-d5a5ccz7.js} +18 -12
- package/dist/server/{index.server-z38bxms8.js → index.server-g5jgq3jp.js} +65 -6
- package/dist/server/{index.server-39ne5cd1.js → index.server-pxq277t5.js} +32 -1
- package/package.json +22 -95
- package/src/components/code-block/code-block.css +15 -9
- package/src/components/code-block/code-block.variables.json +7 -1
- package/src/components/code-block/code-block.variables.ts +7 -1
- package/src/components/confirm-dialog/confirm-dialog.css +2 -0
- package/src/components/confirm-dialog/confirm-dialog.schema.json +8 -0
- package/src/components/confirm-dialog/confirm-dialog.schema.ts +10 -0
- package/src/components/confirm-dialog/confirm-dialog.svelte +31 -3
- package/src/components/confirm-dialog/confirm-dialog.types.ts +7 -0
- package/src/components/faceted-filter-bar/faceted-filter-bar.schema.json +4 -0
- package/src/components/faceted-filter-bar/faceted-filter-bar.schema.ts +4 -0
- package/src/components/faceted-filter-bar/faceted-filter-bar.svelte +13 -11
- package/src/components/faceted-filter-bar/faceted-filter-bar.types.ts +2 -0
- package/src/components/navigation-bar/navigation-bar.svelte +2 -2
- package/src/components/run-step-timeline/run-step-timeline.svelte +4 -4
- package/src/components/run-step-timeline/run-step-timeline.utilities.ts +68 -0
|
@@ -31,6 +31,16 @@ const schema = {
|
|
|
31
31
|
description:
|
|
32
32
|
'When true, the confirm button uses variant="danger". The cancel button still\nreceives default focus regardless — color is never the sole destructive signal.',
|
|
33
33
|
},
|
|
34
|
+
typeToConfirm: {
|
|
35
|
+
type: 'string',
|
|
36
|
+
description:
|
|
37
|
+
'When set, renders a labelled text input and disables the confirm button until the\ntrimmed input matches this value case-insensitively.',
|
|
38
|
+
},
|
|
39
|
+
typeToConfirmLabel: {
|
|
40
|
+
type: 'string',
|
|
41
|
+
description:
|
|
42
|
+
'Visible label for the typed-confirmation input. Defaults to `Type "<value>" to confirm`.',
|
|
43
|
+
},
|
|
34
44
|
class: {
|
|
35
45
|
type: 'string',
|
|
36
46
|
description:
|
|
@@ -4,11 +4,25 @@ export {};
|
|
|
4
4
|
|
|
5
5
|
<script lang="ts">
|
|
6
6
|
import Button from '../button/button.svelte';
|
|
7
|
+
import Input from '../input/input.svelte';
|
|
7
8
|
import Modal from '../modal/modal.svelte';
|
|
8
9
|
import { classNames } from '../../utilities/class-names.ts';
|
|
9
10
|
const descriptionId = $props.id();
|
|
10
|
-
|
|
11
|
+
const typedConfirmationId = `${descriptionId}-typed-confirmation`;
|
|
12
|
+
let { open = $bindable(false), title, description, cancelLabel = 'Cancel', confirmLabel, destructive = false, typeToConfirm, typeToConfirmLabel, onconfirm, oncancel, triggerRef = null, class: className, } = $props();
|
|
11
13
|
const describedById = $derived(description ? descriptionId : undefined);
|
|
14
|
+
const normalizedTypeToConfirm = $derived(typeToConfirm?.trim() || undefined);
|
|
15
|
+
const normalizedTypeToConfirmLabel = $derived(typeToConfirmLabel?.trim() || undefined);
|
|
16
|
+
let typedConfirmation = $state('');
|
|
17
|
+
let previousOpen = open;
|
|
18
|
+
const typedConfirmationMatches = $derived(normalizedTypeToConfirm === undefined ||
|
|
19
|
+
typedConfirmation.trim().toLowerCase() === normalizedTypeToConfirm.toLowerCase());
|
|
20
|
+
$effect(() => {
|
|
21
|
+
if (open === previousOpen)
|
|
22
|
+
return;
|
|
23
|
+
previousOpen = open;
|
|
24
|
+
typedConfirmation = '';
|
|
25
|
+
});
|
|
12
26
|
function handleCancel() {
|
|
13
27
|
// Mirror Modal's dismiss() ordering: state first, then callback. No try/catch —
|
|
14
28
|
// consumer errors must propagate so tests, error boundaries, and observability see them.
|
|
@@ -34,10 +48,24 @@ function handleConfirm() {
|
|
|
34
48
|
<p id={descriptionId} class="cinder-confirm-dialog__description">{description}</p>
|
|
35
49
|
{/if}
|
|
36
50
|
|
|
51
|
+
{#if normalizedTypeToConfirm !== undefined}
|
|
52
|
+
<Input
|
|
53
|
+
id={typedConfirmationId}
|
|
54
|
+
class="cinder-confirm-dialog__typed-confirmation"
|
|
55
|
+
bind:value={typedConfirmation}
|
|
56
|
+
label={normalizedTypeToConfirmLabel ?? `Type "${normalizedTypeToConfirm}" to confirm`}
|
|
57
|
+
autocomplete="off"
|
|
58
|
+
/>
|
|
59
|
+
{/if}
|
|
60
|
+
|
|
37
61
|
{#snippet footer()}
|
|
38
62
|
<Button variant="secondary" autofocus onclick={handleCancel}>{cancelLabel}</Button>
|
|
39
|
-
<Button
|
|
40
|
-
|
|
63
|
+
<Button
|
|
64
|
+
variant={destructive ? 'danger' : 'primary'}
|
|
65
|
+
disabled={!typedConfirmationMatches}
|
|
66
|
+
onclick={handleConfirm}
|
|
41
67
|
>
|
|
68
|
+
{confirmLabel}
|
|
69
|
+
</Button>
|
|
42
70
|
{/snippet}
|
|
43
71
|
</Modal>
|
|
@@ -36,6 +36,13 @@ export type ConfirmDialogProps = {
|
|
|
36
36
|
* receives default focus regardless — color is never the sole destructive signal.
|
|
37
37
|
*/
|
|
38
38
|
destructive?: boolean;
|
|
39
|
+
/**
|
|
40
|
+
* When set, renders a labelled text input and disables the confirm button until the
|
|
41
|
+
* trimmed input matches this value case-insensitively.
|
|
42
|
+
*/
|
|
43
|
+
typeToConfirm?: string;
|
|
44
|
+
/** Visible label for the typed-confirmation input. Defaults to `Type "<value>" to confirm`. */
|
|
45
|
+
typeToConfirmLabel?: string;
|
|
39
46
|
/** Fired when the user activates the confirm button. Required. Component closes itself after. */
|
|
40
47
|
onconfirm: () => void;
|
|
41
48
|
/**
|
|
@@ -10,6 +10,10 @@
|
|
|
10
10
|
"type": "string",
|
|
11
11
|
"description": "Current text search query. When provided, the search field is controlled."
|
|
12
12
|
},
|
|
13
|
+
"showSearch": {
|
|
14
|
+
"type": "boolean",
|
|
15
|
+
"description": "Whether to render the leading search field. Defaults to `true`."
|
|
16
|
+
},
|
|
13
17
|
"searchPlaceholder": {
|
|
14
18
|
"type": "string",
|
|
15
19
|
"description": "Placeholder text shown in the leading search field."
|
|
@@ -12,6 +12,10 @@ const schema = {
|
|
|
12
12
|
type: 'string',
|
|
13
13
|
description: 'Current text search query. When provided, the search field is controlled.',
|
|
14
14
|
},
|
|
15
|
+
showSearch: {
|
|
16
|
+
type: 'boolean',
|
|
17
|
+
description: 'Whether to render the leading search field. Defaults to `true`.',
|
|
18
|
+
},
|
|
15
19
|
searchPlaceholder: {
|
|
16
20
|
type: 'string',
|
|
17
21
|
description: 'Placeholder text shown in the leading search field.',
|
|
@@ -10,7 +10,7 @@ import Chip from '../chip/chip.svelte';
|
|
|
10
10
|
import Button from '../button/button.svelte';
|
|
11
11
|
import { classNames } from '../../utilities/class-names.ts';
|
|
12
12
|
const generatedId = $props.id();
|
|
13
|
-
let { 'aria-label': ariaLabel = 'Filters', searchQuery, searchPlaceholder = 'Search…', searchAriaLabel = 'Search', facets = [], appliedFilters = [], disabled = false, class: className, onsearchchange, onfacetchange, onfilterremove, onclearall, ...rest } = $props();
|
|
13
|
+
let { 'aria-label': ariaLabel = 'Filters', searchQuery, showSearch = true, searchPlaceholder = 'Search…', searchAriaLabel = 'Search', facets = [], appliedFilters = [], disabled = false, class: className, onsearchchange, onfacetchange, onfilterremove, onclearall, ...rest } = $props();
|
|
14
14
|
const searchId = $derived(`${generatedId}-search`);
|
|
15
15
|
let rootElement = $state();
|
|
16
16
|
// Internal uncontrolled search value when searchQuery is not provided.
|
|
@@ -32,7 +32,7 @@ function resolveFilterDisplayValue(key, value) {
|
|
|
32
32
|
return value;
|
|
33
33
|
return facet.options.find((option) => option.value === value)?.label ?? value;
|
|
34
34
|
}
|
|
35
|
-
const hasAppliedFilters = $derived(appliedFilters.length > 0 || currentSearchQuery.length > 0);
|
|
35
|
+
const hasAppliedFilters = $derived(appliedFilters.length > 0 || (showSearch && currentSearchQuery.length > 0));
|
|
36
36
|
const totalActiveCount = $derived(appliedFilters.length);
|
|
37
37
|
// Live region summary message — always in DOM, content changes when filters change.
|
|
38
38
|
const summaryMessage = $derived.by(() => {
|
|
@@ -81,15 +81,17 @@ function getFacetCurrentValue(key) {
|
|
|
81
81
|
>
|
|
82
82
|
<!-- Controls row: search field + facet selects -->
|
|
83
83
|
<div class="cinder-faceted-filter-bar__controls">
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
84
|
+
{#if showSearch}
|
|
85
|
+
<SearchField
|
|
86
|
+
id={searchId}
|
|
87
|
+
class="cinder-faceted-filter-bar__search"
|
|
88
|
+
value={currentSearchQuery}
|
|
89
|
+
placeholder={searchPlaceholder}
|
|
90
|
+
aria-label={searchAriaLabel}
|
|
91
|
+
{disabled}
|
|
92
|
+
oninput={handleSearchInput}
|
|
93
|
+
/>
|
|
94
|
+
{/if}
|
|
93
95
|
|
|
94
96
|
{#each facets as facet (facet.key)}
|
|
95
97
|
{#if facet.type === 'select'}
|
|
@@ -69,6 +69,8 @@ export type FacetedFilterBarProps = Omit<HTMLAttributes<HTMLDivElement>, 'class'
|
|
|
69
69
|
'aria-label'?: string;
|
|
70
70
|
/** Current text search query. When provided, the search field is controlled. */
|
|
71
71
|
searchQuery?: string;
|
|
72
|
+
/** Whether to render the leading search field. Defaults to `true`. */
|
|
73
|
+
showSearch?: boolean;
|
|
72
74
|
/** Placeholder text shown in the leading search field. */
|
|
73
75
|
searchPlaceholder?: string;
|
|
74
76
|
/** Accessible label for the search input. Defaults to 'Search'. */
|
|
@@ -208,7 +208,7 @@ function handleKeyDown(event) {
|
|
|
208
208
|
{@render menuToggle({
|
|
209
209
|
'aria-expanded': (mobileMenuOpen ? 'true' : 'false') as 'true' | 'false',
|
|
210
210
|
'aria-controls': regionId,
|
|
211
|
-
|
|
211
|
+
onclick: handleToggle,
|
|
212
212
|
})}
|
|
213
213
|
</div>
|
|
214
214
|
{/if}
|
|
@@ -224,7 +224,7 @@ function handleKeyDown(event) {
|
|
|
224
224
|
{@render menuToggle({
|
|
225
225
|
'aria-expanded': (mobileMenuOpen ? 'true' : 'false') as 'true' | 'false',
|
|
226
226
|
'aria-controls': regionId,
|
|
227
|
-
|
|
227
|
+
onclick: handleToggle,
|
|
228
228
|
})}
|
|
229
229
|
</div>
|
|
230
230
|
{/if}
|
|
@@ -10,11 +10,11 @@ import RunStepBranchDisclosure from './run-step-branch-disclosure.svelte';
|
|
|
10
10
|
import Link from '../link/link.svelte';
|
|
11
11
|
import Progress from '../progress/progress.svelte';
|
|
12
12
|
import StatusDot from '../status-dot/status-dot.svelte';
|
|
13
|
-
import { actionsCountLabel, badgeVariant, branchGroupHasCurrentStep, branchOutcomeSummary, branchStartsCollapsed, hasProgress, hiddenNestedStepLabel, isCurrent, isTerminal, laneOutcomeBadgeVariant, laneOutcomeLabel, metadataItems, safeStepLinkHref, statusDotStatus, statusLabel, } from './run-step-timeline.utilities.ts';
|
|
13
|
+
import { actionsCountLabel, badgeVariant, branchGroupHasCurrentStep, branchOutcomeSummary, branchStartsCollapsed, hasProgress, hiddenNestedStepLabel, isCurrent, isTerminal, laneOutcomeBadgeVariant, laneOutcomeLabel, metadataItems, relocateCompensationEntries, relocateCompensationSteps, safeStepLinkHref, statusDotStatus, statusLabel, } from './run-step-timeline.utilities.ts';
|
|
14
14
|
const MAX_NESTED_STEP_DEPTH = 3;
|
|
15
15
|
let { steps, label, class: className, children, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledby, ...rest } = $props();
|
|
16
16
|
const resolvedAriaLabel = $derived(ariaLabelledby === undefined && ariaLabel === undefined ? label : ariaLabel);
|
|
17
|
-
const renderedEntries = $derived(flattenEntries(steps));
|
|
17
|
+
const renderedEntries = $derived(flattenEntries(relocateCompensationEntries(steps)));
|
|
18
18
|
function isBranchGroup(entry) {
|
|
19
19
|
return 'kind' in entry && entry.kind === 'branch';
|
|
20
20
|
}
|
|
@@ -128,7 +128,7 @@ function applyGlobalRailAriaCurrent(rows) {
|
|
|
128
128
|
// compensation labels. Reused for the main rail and for each branch lane.
|
|
129
129
|
function flattenSteps(list, pathPrefix) {
|
|
130
130
|
const rows = [];
|
|
131
|
-
appendRunStepRows(rows, list, 0, pathPrefix);
|
|
131
|
+
appendRunStepRows(rows, relocateCompensationSteps(list), 0, pathPrefix);
|
|
132
132
|
const currentRowIndex = deepestCurrentStepIndex(rows);
|
|
133
133
|
// Index every step row by its unique path key (built from the escaped id
|
|
134
134
|
// chain). A `compensates` id then resolves to a true SIBLING — a step under
|
|
@@ -174,7 +174,7 @@ function appendRunStepRows(rows, list, depth, pathPrefix) {
|
|
|
174
174
|
rows.push({ kind: 'step', step, depth, pathKey });
|
|
175
175
|
if (step.children && step.children.length > 0) {
|
|
176
176
|
if (depth < MAX_NESTED_STEP_DEPTH) {
|
|
177
|
-
appendRunStepRows(rows, step.children, depth + 1, pathKey);
|
|
177
|
+
appendRunStepRows(rows, relocateCompensationSteps(step.children), depth + 1, pathKey);
|
|
178
178
|
}
|
|
179
179
|
else {
|
|
180
180
|
const hiddenSummary = summarizeNestedRunSteps(step.children);
|
|
@@ -5,8 +5,76 @@ import type {
|
|
|
5
5
|
RunStepBranchLane,
|
|
6
6
|
RunStepBranchLaneOutcome,
|
|
7
7
|
RunStepStatus,
|
|
8
|
+
RunStepTimelineEntry,
|
|
8
9
|
} from './run-step-timeline.types.ts';
|
|
9
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Relocate resolved compensation steps immediately after the subtree of the
|
|
13
|
+
* sibling they reverse. Unresolved, self-referential, and cyclic links retain
|
|
14
|
+
* consumer order. Multiple compensations retain their relative input order.
|
|
15
|
+
*/
|
|
16
|
+
export function relocateCompensationSteps(steps: RunStep[]): RunStep[] {
|
|
17
|
+
return relocateSiblingItems(steps, (step) => step);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function relocateSiblingItems<T>(items: T[], getStep: (item: T) => RunStep | undefined): T[] {
|
|
21
|
+
if (!items.some((item) => getStep(item)?.compensates !== undefined)) return items;
|
|
22
|
+
|
|
23
|
+
const stepById = new Map<string, RunStep>();
|
|
24
|
+
for (const item of items) {
|
|
25
|
+
const step = getStep(item);
|
|
26
|
+
if (step !== undefined) {
|
|
27
|
+
stepById.set(step.id, step);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const canRelocate = (step: RunStep): boolean => {
|
|
31
|
+
const visited = new Set<string>([step.id]);
|
|
32
|
+
let targetId = step.compensates;
|
|
33
|
+
while (targetId !== undefined) {
|
|
34
|
+
const target = stepById.get(targetId);
|
|
35
|
+
if (target === undefined || visited.has(targetId)) return false;
|
|
36
|
+
visited.add(targetId);
|
|
37
|
+
targetId = target.compensates;
|
|
38
|
+
}
|
|
39
|
+
return true;
|
|
40
|
+
};
|
|
41
|
+
const relocatable = new Set<RunStep>();
|
|
42
|
+
const compensationsByTarget = new Map<string, T[]>();
|
|
43
|
+
for (const item of items) {
|
|
44
|
+
const step = getStep(item);
|
|
45
|
+
if (step === undefined || step.compensates === undefined || !canRelocate(step)) continue;
|
|
46
|
+
relocatable.add(step);
|
|
47
|
+
const compensations = compensationsByTarget.get(step.compensates) ?? [];
|
|
48
|
+
compensations.push(item);
|
|
49
|
+
compensationsByTarget.set(step.compensates, compensations);
|
|
50
|
+
}
|
|
51
|
+
const result: T[] = [];
|
|
52
|
+
const appendWithCompensations = (item: T): void => {
|
|
53
|
+
result.push(item);
|
|
54
|
+
const step = getStep(item);
|
|
55
|
+
if (step === undefined) return;
|
|
56
|
+
for (const compensation of compensationsByTarget.get(step.id) ?? []) {
|
|
57
|
+
appendWithCompensations(compensation);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
for (const item of items) {
|
|
61
|
+
const step = getStep(item);
|
|
62
|
+
if (step === undefined || !relocatable.has(step)) appendWithCompensations(item);
|
|
63
|
+
}
|
|
64
|
+
return result;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Relocate top-level compensations while preserving branch rows as rail entries. */
|
|
68
|
+
export function relocateCompensationEntries(
|
|
69
|
+
entries: RunStepTimelineEntry[],
|
|
70
|
+
): RunStepTimelineEntry[] {
|
|
71
|
+
return relocateSiblingItems(entries, (entry) => (isBranchGroup(entry) ? undefined : entry));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function isBranchGroup(entry: RunStepTimelineEntry): entry is RunStepBranchGroup {
|
|
75
|
+
return 'kind' in entry && entry.kind === 'branch';
|
|
76
|
+
}
|
|
77
|
+
|
|
10
78
|
/** Badge variants used for the per-step status chip. */
|
|
11
79
|
export type RunStepBadgeVariant = 'neutral' | 'success' | 'warning' | 'danger' | 'info' | 'accent';
|
|
12
80
|
|