cursedbelt 5.55.2 → 5.58.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/dist/react/charts/tokens.js +4 -4
- package/dist/react/code-editor/CodeEditor.d.ts.map +1 -1
- package/dist/react/code-editor/CodeEditor.js +2 -1
- package/dist/react/code-editor/CodeEditor.js.map +1 -1
- package/dist/react/code-editor/lineAbove.d.ts +17 -0
- package/dist/react/code-editor/lineAbove.d.ts.map +1 -0
- package/dist/react/code-editor/lineAbove.js +34 -0
- package/dist/react/code-editor/lineAbove.js.map +1 -0
- package/dist/react/companion-link/CompanionLinkStatus.d.ts.map +1 -1
- package/dist/react/companion-link/CompanionLinkStatus.js +2 -1
- package/dist/react/companion-link/CompanionLinkStatus.js.map +1 -1
- package/dist/react/components/Accordion.d.ts.map +1 -1
- package/dist/react/components/Accordion.js +2 -2
- package/dist/react/components/Accordion.js.map +1 -1
- package/dist/react/components/Badge.d.ts +20 -1
- package/dist/react/components/Badge.d.ts.map +1 -1
- package/dist/react/components/Badge.js +30 -3
- package/dist/react/components/Badge.js.map +1 -1
- package/dist/react/components/Button.d.ts.map +1 -1
- package/dist/react/components/Button.js +8 -2
- package/dist/react/components/Button.js.map +1 -1
- package/dist/react/components/Collapsible.d.ts.map +1 -1
- package/dist/react/components/Collapsible.js +2 -2
- package/dist/react/components/Collapsible.js.map +1 -1
- package/dist/react/components/DisclosureButton.d.ts +2 -2
- package/dist/react/components/DisclosureButton.d.ts.map +1 -1
- package/dist/react/components/DisclosureButton.js +9 -6
- package/dist/react/components/DisclosureButton.js.map +1 -1
- package/dist/react/components/EditorPage.d.ts +78 -0
- package/dist/react/components/EditorPage.d.ts.map +1 -0
- package/dist/react/components/EditorPage.js +133 -0
- package/dist/react/components/EditorPage.js.map +1 -0
- package/dist/react/components/IconPicker.d.ts +70 -0
- package/dist/react/components/IconPicker.d.ts.map +1 -0
- package/dist/react/components/IconPicker.js +175 -0
- package/dist/react/components/IconPicker.js.map +1 -0
- package/dist/react/components/PropertyList.d.ts +45 -0
- package/dist/react/components/PropertyList.d.ts.map +1 -0
- package/dist/react/components/PropertyList.js +12 -0
- package/dist/react/components/PropertyList.js.map +1 -0
- package/dist/react/components/Wizard/ReviewStep.d.ts +1 -1
- package/dist/react/components/Wizard/ReviewStep.d.ts.map +1 -1
- package/dist/react/components/Wizard/ReviewStep.js +3 -3
- package/dist/react/components/Wizard/ReviewStep.js.map +1 -1
- package/dist/react/components/editorPage.css +421 -0
- package/dist/react/components/formKit.d.ts +3 -1
- package/dist/react/components/formKit.d.ts.map +1 -1
- package/dist/react/components/formKit.js +3 -1
- package/dist/react/components/formKit.js.map +1 -1
- package/dist/react/file-tree/FileTree.d.ts.map +1 -1
- package/dist/react/file-tree/FileTree.js +7 -0
- package/dist/react/file-tree/FileTree.js.map +1 -1
- package/dist/react/file-tree/fileTree.css +14 -0
- package/dist/react/filter-rail/FilterRail.js +1 -1
- package/dist/react/filter-rail/FilterRail.js.map +1 -1
- package/dist/react/filter-rail/RailSection.js +2 -2
- package/dist/react/filter-rail/RailSection.js.map +1 -1
- package/dist/react/filter-rail/headerContextMenu.d.ts.map +1 -1
- package/dist/react/filter-rail/headerContextMenu.js +20 -1
- package/dist/react/filter-rail/headerContextMenu.js.map +1 -1
- package/dist/react/hooks/longPressContextMenu.d.ts +35 -0
- package/dist/react/hooks/longPressContextMenu.d.ts.map +1 -0
- package/dist/react/hooks/longPressContextMenu.js +136 -0
- package/dist/react/hooks/longPressContextMenu.js.map +1 -0
- package/dist/react/hooks/useLongPressContextMenu.d.ts +4 -0
- package/dist/react/hooks/useLongPressContextMenu.d.ts.map +1 -0
- package/dist/react/hooks/useLongPressContextMenu.js +12 -0
- package/dist/react/hooks/useLongPressContextMenu.js.map +1 -0
- package/dist/react/index.d.ts +4 -1
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +3 -0
- package/dist/react/index.js.map +1 -1
- package/dist/react/line-keys/index.d.ts +2 -0
- package/dist/react/line-keys/index.d.ts.map +1 -0
- package/dist/react/line-keys/index.js +2 -0
- package/dist/react/line-keys/index.js.map +1 -0
- package/dist/react/line-keys/lineKeys.d.ts +62 -0
- package/dist/react/line-keys/lineKeys.d.ts.map +1 -0
- package/dist/react/line-keys/lineKeys.js +159 -0
- package/dist/react/line-keys/lineKeys.js.map +1 -0
- package/dist/react/master-detail/DetailPane.d.ts +3 -0
- package/dist/react/master-detail/DetailPane.d.ts.map +1 -1
- package/dist/react/master-detail/DetailPane.js +5 -2
- package/dist/react/master-detail/DetailPane.js.map +1 -1
- package/dist/react/nav/SideNav.js +1 -1
- package/dist/react/nav/SideNav.js.map +1 -1
- package/dist/scripts/guardrailsEnforce.d.ts.map +1 -1
- package/dist/styles-areas/analytics.css +14 -14
- package/dist/styles-areas/charts.css +3 -3
- package/dist/styles-areas/companion-link.css +2 -2
- package/dist/styles-areas/core.css +50 -45
- package/dist/styles-areas/data-table.css +15 -16
- package/dist/styles-areas/disk-usage.css +4 -4
- package/dist/styles-areas/fields.css +11 -11
- package/dist/styles-areas/folder-tree.css +3 -3
- package/dist/styles-areas/layout-engine.css +2 -2
- package/dist/styles-areas/master-detail.css +4 -5
- package/dist/styles-areas/media-gallery.css +9 -9
- package/dist/styles-areas/media.css +3 -3
- package/dist/styles-areas/nav.css +5 -5
- package/dist/styles-areas/notifications.css +3 -3
- package/dist/styles-areas/problem-detail.css +3 -3
- package/dist/styles-areas/sharing.css +2 -3
- package/dist/styles-areas/stats.css +3 -3
- package/dist/styles-areas/wizard.css +7 -8
- package/dist/styles-areas/workbench.css +4 -4
- package/dist/styles-utilities.css +49 -10
- package/dist/theme.css +38 -22
- package/package.json +14 -2
- package/scripts/guardrails-baseline.json +2 -1
- package/scripts/guardrailsEnforce.spec.ts +32 -0
- package/scripts/guardrailsEnforce.ts +67 -1
- package/scripts/styleAreas.ts +3 -0
- package/src/react/charts/tokens.ts +4 -4
- package/src/react/code-editor/CodeEditor.tsx +2 -1
- package/src/react/code-editor/lineAbove.spec.ts +36 -0
- package/src/react/code-editor/lineAbove.ts +35 -0
- package/src/react/companion-link/CompanionLinkStatus.tsx +10 -12
- package/src/react/components/Accordion.tsx +5 -4
- package/src/react/components/Badge.capsule.spec.ts +1 -1
- package/src/react/components/Badge.spec.tsx +22 -1
- package/src/react/components/Badge.tsx +66 -11
- package/src/react/components/Button.spec.tsx +5 -4
- package/src/react/components/Button.tsx +8 -2
- package/src/react/components/Collapsible.tsx +5 -4
- package/src/react/components/DisclosureButton.spec.tsx +47 -0
- package/src/react/components/DisclosureButton.tsx +11 -7
- package/src/react/components/EditorPage.spec.tsx +106 -0
- package/src/react/components/EditorPage.tsx +281 -0
- package/src/react/components/IconPicker.spec.tsx +84 -0
- package/src/react/components/IconPicker.tsx +289 -0
- package/src/react/components/PropertyList.spec.tsx +110 -0
- package/src/react/components/PropertyList.tsx +92 -0
- package/src/react/components/Wizard/ReviewStep.tsx +7 -7
- package/src/react/components/editorPage.css +421 -0
- package/src/react/components/formKit.ts +10 -1
- package/src/react/components/formKitStylesheets.spec.ts +47 -0
- package/src/react/file-tree/FileTree.spec.tsx +19 -0
- package/src/react/file-tree/FileTree.tsx +8 -0
- package/src/react/file-tree/fileTree.css +14 -0
- package/src/react/filter-rail/FilterRail.tsx +1 -1
- package/src/react/filter-rail/RailSection.tsx +2 -2
- package/src/react/filter-rail/headerContextMenu.spec.tsx +52 -0
- package/src/react/filter-rail/headerContextMenu.ts +19 -1
- package/src/react/hooks/longPressContextMenu.spec.ts +102 -0
- package/src/react/hooks/longPressContextMenu.ts +135 -0
- package/src/react/hooks/useLongPressContextMenu.ts +15 -0
- package/src/react/index.ts +11 -1
- package/src/react/line-keys/index.ts +1 -0
- package/src/react/line-keys/lineKeys.spec.tsx +176 -0
- package/src/react/line-keys/lineKeys.ts +176 -0
- package/src/react/master-detail/DetailPane.tsx +14 -18
- package/src/react/nav/SideNav.tsx +1 -1
- package/src/styles-areas/analytics.css +14 -14
- package/src/styles-areas/charts.css +3 -3
- package/src/styles-areas/companion-link.css +2 -2
- package/src/styles-areas/core.css +50 -45
- package/src/styles-areas/data-table.css +15 -16
- package/src/styles-areas/disk-usage.css +4 -4
- package/src/styles-areas/fields.css +11 -11
- package/src/styles-areas/folder-tree.css +3 -3
- package/src/styles-areas/layout-engine.css +2 -2
- package/src/styles-areas/master-detail.css +4 -5
- package/src/styles-areas/media-gallery.css +9 -9
- package/src/styles-areas/media.css +3 -3
- package/src/styles-areas/nav.css +5 -5
- package/src/styles-areas/notifications.css +3 -3
- package/src/styles-areas/problem-detail.css +3 -3
- package/src/styles-areas/sharing.css +2 -3
- package/src/styles-areas/stats.css +3 -3
- package/src/styles-areas/wizard.css +7 -8
- package/src/styles-areas/workbench.css +4 -4
- package/src/styles-utilities.css +49 -10
- package/src/theme.css +38 -22
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from 'bun:test';
|
|
2
|
+
import { cleanup, render } from '@testing-library/react';
|
|
3
|
+
import { FactList } from '../master-detail/DetailPane.js';
|
|
4
|
+
import { Property, PropertyList } from './PropertyList.js';
|
|
5
|
+
|
|
6
|
+
afterEach(cleanup);
|
|
7
|
+
|
|
8
|
+
describe('PropertyList', () => {
|
|
9
|
+
test('is a real definition list: one <dt>/<dd> pair per fact, named by aria-label', () => {
|
|
10
|
+
const { container, getByLabelText } = render(
|
|
11
|
+
<PropertyList aria-label='List template'>
|
|
12
|
+
<Property label='Opens as'>Checklist</Property>
|
|
13
|
+
<Property label='Archive'>Kept</Property>
|
|
14
|
+
</PropertyList>,
|
|
15
|
+
);
|
|
16
|
+
expect(getByLabelText('List template').tagName).toBe('DL');
|
|
17
|
+
expect([...container.querySelectorAll('dt')].map((el) => el.textContent)).toEqual(['Opens as', 'Archive']);
|
|
18
|
+
expect([...container.querySelectorAll('dd')].map((el) => el.textContent)).toEqual(['Checklist', 'Kept']);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('the label column is ONE shared track sized by the token, never by a row (FRM-1, FRM-2)', () => {
|
|
22
|
+
const { container } = render(
|
|
23
|
+
<PropertyList>
|
|
24
|
+
<Property label='A'>1</Property>
|
|
25
|
+
</PropertyList>,
|
|
26
|
+
);
|
|
27
|
+
expect(container.querySelector('dl')?.className).toContain('grid-cols-[minmax(0,var(--property-label-w))_minmax(0,1fr)]');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('🔴 a row draws no fill, no border and no radius — the boxes the owner lost to Linear on', () => {
|
|
31
|
+
const { container } = render(
|
|
32
|
+
<PropertyList>
|
|
33
|
+
<Property label='Opens as'>Checklist</Property>
|
|
34
|
+
</PropertyList>,
|
|
35
|
+
);
|
|
36
|
+
for (const el of container.querySelectorAll('dl, dl *')) {
|
|
37
|
+
expect(el.className).not.toMatch(/(^|\s)(bg-|border|rounded)/);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('the label is muted and regular weight; the value is the full-strength text', () => {
|
|
42
|
+
const { container } = render(
|
|
43
|
+
<PropertyList>
|
|
44
|
+
<Property label='Opens as'>Checklist</Property>
|
|
45
|
+
</PropertyList>,
|
|
46
|
+
);
|
|
47
|
+
const dt = container.querySelector('dt');
|
|
48
|
+
expect(dt?.className).toContain('text-[var(--muted-foreground)]');
|
|
49
|
+
expect(dt?.className).toContain('font-normal');
|
|
50
|
+
expect(container.querySelector('dd > span')?.className).toContain('text-[var(--foreground)]');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('rows sit on the small control height, so a control value lines up with a text one (FND-4)', () => {
|
|
54
|
+
const { container } = render(
|
|
55
|
+
<PropertyList>
|
|
56
|
+
<Property label='Opens as'>Checklist</Property>
|
|
57
|
+
</PropertyList>,
|
|
58
|
+
);
|
|
59
|
+
expect(container.querySelector('dt')?.className).toContain('min-h-[var(--control-h-sm)]');
|
|
60
|
+
expect(container.querySelector('dd')?.className).toContain('min-h-[var(--control-h-sm)]');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('an absent value renders no row at all; `empty` keeps the row and mutes it', () => {
|
|
64
|
+
const { container } = render(
|
|
65
|
+
<PropertyList>
|
|
66
|
+
<Property label='Gone'>{''}</Property>
|
|
67
|
+
<Property label='Null'>{null}</Property>
|
|
68
|
+
<Property label='Lead' empty>
|
|
69
|
+
Add lead
|
|
70
|
+
</Property>
|
|
71
|
+
</PropertyList>,
|
|
72
|
+
);
|
|
73
|
+
expect(container.querySelectorAll('dt')).toHaveLength(1);
|
|
74
|
+
expect(container.querySelector('dd > span')?.className).toContain('text-[var(--muted-foreground)]');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('a mono value breaks anywhere; prose breaks between words (task 2593)', () => {
|
|
78
|
+
const { getByText } = render(
|
|
79
|
+
<PropertyList>
|
|
80
|
+
<Property label='Path' mono>
|
|
81
|
+
/a/b
|
|
82
|
+
</Property>
|
|
83
|
+
<Property label='Note'>the recovery codes</Property>
|
|
84
|
+
</PropertyList>,
|
|
85
|
+
);
|
|
86
|
+
expect(getByText('/a/b').className).toContain('break-all');
|
|
87
|
+
expect(getByText('the recovery codes').className).toContain('break-words');
|
|
88
|
+
expect(getByText('the recovery codes').className).not.toContain('break-all');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('icon and action stay inside the fact\'s own row (FRM-6)', () => {
|
|
92
|
+
const { container } = render(
|
|
93
|
+
<PropertyList>
|
|
94
|
+
<Property label='Status' icon={<svg data-testid='ring' />} action={<span data-testid='copy'>Copy</span>}>
|
|
95
|
+
Planned
|
|
96
|
+
</Property>
|
|
97
|
+
</PropertyList>,
|
|
98
|
+
);
|
|
99
|
+
const dd = container.querySelector('dd');
|
|
100
|
+
expect(dd?.querySelector('[data-testid="ring"]')).not.toBeNull();
|
|
101
|
+
expect(dd?.querySelector('[data-testid="copy"]')?.textContent).toBe('Copy');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('FactList IS a compact PropertyList — one primitive draws every label/value pair', () => {
|
|
105
|
+
const { container } = render(<FactList facts={[{ label: 'Port', value: '3207' }, { label: 'Blank', value: '' }]} />);
|
|
106
|
+
const dl = container.querySelector('dl');
|
|
107
|
+
expect(dl?.getAttribute('data-density')).toBe('compact');
|
|
108
|
+
expect(container.querySelectorAll('dt')).toHaveLength(1);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `PropertyList` — one record's facts as Linear draws them: a quiet label column, the values in
|
|
3
|
+
* a second column, and nothing else. No row fills, no boxes, no rules between rows.
|
|
4
|
+
*
|
|
5
|
+
* ── The failure it exists to stop ───────────────────────────────────────────
|
|
6
|
+
* The owner put desk's Components page under Linear's project page and asked a judge which had
|
|
7
|
+
* the better UI (2026-10-05). desk lost, and the judge's words for its facts were "tightly boxed
|
|
8
|
+
* container shapes, stark block borders": five label/value rows, each in its own filled,
|
|
9
|
+
* rounded box, with the label in bold. Linear's Properties panel carries nine rows in less ink:
|
|
10
|
+
* the label is muted and regular weight, the value is the only full-strength text in the row,
|
|
11
|
+
* and the rows are separated by nothing but their own height. Every app that hand-rolls a `<dl>`
|
|
12
|
+
* makes desk's choice again, so the shape lives here once and `no-hand-rolled-property-list`
|
|
13
|
+
* refuses the raw element in app source.
|
|
14
|
+
*
|
|
15
|
+
* The numbers are the design law's: one shared label column (FRM-1, FRM-2) sized by a token,
|
|
16
|
+
* rows on the control height so a value that IS a control (a select, a button) lines up with
|
|
17
|
+
* one that is text (FND-4), two type sizes at most (FND-5), and text that never drops under
|
|
18
|
+
* 12px (FND-6).
|
|
19
|
+
*/
|
|
20
|
+
import type { HTMLAttributes, ReactNode } from "react";
|
|
21
|
+
import { cn } from "../lib/cn.js";
|
|
22
|
+
|
|
23
|
+
export interface PropertyListProps extends HTMLAttributes<HTMLDListElement> {
|
|
24
|
+
/**
|
|
25
|
+
* `comfortable` (default) is Linear's panel: 14px text on 32px rows. `compact` is 12px text
|
|
26
|
+
* on 24px rows, for a dense detail pane that lists paths and ids.
|
|
27
|
+
*/
|
|
28
|
+
density?: "comfortable" | "compact";
|
|
29
|
+
children: ReactNode;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function PropertyList({ density = "comfortable", className, children, ...props }: PropertyListProps) {
|
|
33
|
+
return (
|
|
34
|
+
<dl
|
|
35
|
+
data-density={density}
|
|
36
|
+
className={cn(
|
|
37
|
+
"group/properties m-0 grid grid-cols-[minmax(0,var(--property-label-w))_minmax(0,1fr)] gap-x-4",
|
|
38
|
+
density === "compact" ? "gap-y-1 text-xs" : "gap-y-0 text-sm",
|
|
39
|
+
className,
|
|
40
|
+
)}
|
|
41
|
+
{...props}
|
|
42
|
+
>
|
|
43
|
+
{children}
|
|
44
|
+
</dl>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface PropertyProps {
|
|
49
|
+
/** What the fact is. Plain words, sentence case (CPY-9). */
|
|
50
|
+
label: ReactNode;
|
|
51
|
+
/** A 16px mark before the value — Linear's status ring, a person, a calendar. */
|
|
52
|
+
icon?: ReactNode;
|
|
53
|
+
/** The value is not set: drawn muted, the way Linear draws "Add lead". */
|
|
54
|
+
empty?: boolean;
|
|
55
|
+
/** A monospace value with no word to break at (a path, an id) breaks anywhere. */
|
|
56
|
+
mono?: boolean;
|
|
57
|
+
/** Trailing controls that belong to this fact — a copy button, a reset (FRM-6). */
|
|
58
|
+
action?: ReactNode;
|
|
59
|
+
children: ReactNode;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** One fact. Renders nothing when its value is absent, so a caller never filters by hand. */
|
|
63
|
+
export function Property({ label, icon, empty = false, mono = false, action, children }: PropertyProps) {
|
|
64
|
+
if (children === undefined || children === null || children === "" || children === false) return null;
|
|
65
|
+
return (
|
|
66
|
+
<div className="contents">
|
|
67
|
+
<dt className="m-0 flex min-h-[var(--control-h-sm)] min-w-0 items-center font-normal text-[var(--muted-foreground)] group-data-[density=compact]/properties:min-h-6 group-data-[density=compact]/properties:items-start">
|
|
68
|
+
<span className="truncate">{label}</span>
|
|
69
|
+
</dt>
|
|
70
|
+
<dd className="m-0 flex min-h-[var(--control-h-sm)] min-w-0 items-center gap-2 group-data-[density=compact]/properties:min-h-6 group-data-[density=compact]/properties:items-start">
|
|
71
|
+
{icon ? (
|
|
72
|
+
<span aria-hidden="true" className="flex size-4 flex-none items-center justify-center text-[var(--muted-foreground)]">
|
|
73
|
+
{icon}
|
|
74
|
+
</span>
|
|
75
|
+
) : null}
|
|
76
|
+
{/* break-all only for a mono id, which has no word to break at; prose breaks between
|
|
77
|
+
words — "the re/covery codes" broke mid-word when every value was break-all (task 2593). */}
|
|
78
|
+
<span
|
|
79
|
+
data-slot="property-value"
|
|
80
|
+
className={cn(
|
|
81
|
+
"min-w-0",
|
|
82
|
+
mono ? "break-all font-mono" : "break-words",
|
|
83
|
+
empty ? "text-[var(--muted-foreground)]" : "text-[var(--foreground)]",
|
|
84
|
+
)}
|
|
85
|
+
>
|
|
86
|
+
{children}
|
|
87
|
+
</span>
|
|
88
|
+
{action ? <span className="flex flex-none items-center gap-1">{action}</span> : null}
|
|
89
|
+
</dd>
|
|
90
|
+
</div>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
2
|
import { type FieldValues, get, type UseFormReturn } from 'react-hook-form';
|
|
3
3
|
import type { FieldSpec } from '../FieldInput.js';
|
|
4
4
|
import { Stack } from '../layout.js';
|
|
5
|
+
import { Property, PropertyList } from '../PropertyList.js';
|
|
5
6
|
import { Section } from '../Section.js';
|
|
6
7
|
import { Surface } from '../Surface.js';
|
|
7
8
|
import type { StepDef } from './types.js';
|
|
@@ -58,14 +59,13 @@ export function ReviewStep<T extends FieldValues>({
|
|
|
58
59
|
{steps.map((s) => (
|
|
59
60
|
<Surface key={s.id} level='flat' pad='sm'>
|
|
60
61
|
<Section title={s.title}>
|
|
61
|
-
<
|
|
62
|
+
<PropertyList>
|
|
62
63
|
{s.fields.map((key) => (
|
|
63
|
-
<
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
</Fragment>
|
|
64
|
+
<Property key={key} label={labelFor(key)}>
|
|
65
|
+
{valueFor(key)}
|
|
66
|
+
</Property>
|
|
67
67
|
))}
|
|
68
|
-
</
|
|
68
|
+
</PropertyList>
|
|
69
69
|
</Section>
|
|
70
70
|
</Surface>
|
|
71
71
|
))}
|
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The creation page's own look — `EditorPage`, `QuietForm` and `IconPicker`. Prefix `cbfk-`, and
|
|
3
|
+
* the same layering rules as `formKit.css`, which says why.
|
|
4
|
+
*
|
|
5
|
+
* 🔴 Its own file, imported only by `EditorPage.tsx` and `IconPicker.tsx`. It lived at the end of
|
|
6
|
+
* `formKit.css` for one unpublished version, and every component that imports that file
|
|
7
|
+
* (`KeyBadge`, `ExpandableRow`, …) carried it: station's DuplicatesView and music's TubeView
|
|
8
|
+
* — one KeyBadge each — grew 1.84 → 2.90 kB gzip of a page neither draws (forge task 2832,
|
|
9
|
+
* measured by tools/consumer-preflight.ts). `src/react/components/formKitStylesheets.spec.ts`
|
|
10
|
+
* keeps the split.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/*
|
|
14
|
+
* ── EditorPage ─────────────────────────────────────────────────────────────
|
|
15
|
+
*
|
|
16
|
+
* A creation page: header, then form | divider | preview. Layered like the rest of this file.
|
|
17
|
+
*/
|
|
18
|
+
@layer base {
|
|
19
|
+
.cbfk-page {
|
|
20
|
+
position: relative;
|
|
21
|
+
container-type: inline-size;
|
|
22
|
+
box-sizing: border-box;
|
|
23
|
+
display: flex;
|
|
24
|
+
flex-direction: column;
|
|
25
|
+
min-width: 0;
|
|
26
|
+
min-height: 0;
|
|
27
|
+
background-color: var(--background);
|
|
28
|
+
color: var(--foreground);
|
|
29
|
+
outline: none;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.cbfk-page[data-presentation="overlay"] {
|
|
33
|
+
position: fixed;
|
|
34
|
+
inset: 0;
|
|
35
|
+
z-index: var(--z-overlay, 1000);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.cbfk-page[data-presentation="inline"] {
|
|
39
|
+
height: 100%;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.cbfk-page-head {
|
|
43
|
+
display: flex;
|
|
44
|
+
flex: none;
|
|
45
|
+
align-items: center;
|
|
46
|
+
gap: 0.5rem;
|
|
47
|
+
min-height: 3rem;
|
|
48
|
+
padding: 0.5rem max(0.75rem, env(safe-area-inset-right)) 0.5rem max(0.75rem, env(safe-area-inset-left));
|
|
49
|
+
padding-top: max(0.5rem, env(safe-area-inset-top));
|
|
50
|
+
border-bottom: 1px solid var(--border);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.cbfk-page-back {
|
|
54
|
+
display: inline-flex;
|
|
55
|
+
flex: none;
|
|
56
|
+
align-items: center;
|
|
57
|
+
gap: 0.375rem;
|
|
58
|
+
min-height: 2.25rem;
|
|
59
|
+
margin: 0;
|
|
60
|
+
padding: 0 0.5rem;
|
|
61
|
+
border: 0;
|
|
62
|
+
border-radius: var(--radius-sm, 0.375rem);
|
|
63
|
+
background: transparent;
|
|
64
|
+
color: var(--muted-foreground);
|
|
65
|
+
font: inherit;
|
|
66
|
+
font-size: 0.875rem;
|
|
67
|
+
cursor: pointer;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.cbfk-page-back:hover {
|
|
71
|
+
background-color: var(--muted);
|
|
72
|
+
color: var(--foreground);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.cbfk-page-back:focus-visible,
|
|
76
|
+
.cbfk-page-mode:focus-visible,
|
|
77
|
+
.cbfk-page-divider:focus-visible {
|
|
78
|
+
outline: 2px solid var(--ring);
|
|
79
|
+
outline-offset: -2px;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.cbfk-page-back svg {
|
|
83
|
+
width: 1rem;
|
|
84
|
+
height: 1rem;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.cbfk-page-sep {
|
|
88
|
+
flex: none;
|
|
89
|
+
color: var(--muted-foreground);
|
|
90
|
+
opacity: 0.6;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.cbfk-page-title {
|
|
94
|
+
flex: 1 1 auto;
|
|
95
|
+
min-width: 0;
|
|
96
|
+
margin: 0;
|
|
97
|
+
overflow: hidden;
|
|
98
|
+
font-size: 0.9375rem;
|
|
99
|
+
font-weight: 500;
|
|
100
|
+
white-space: nowrap;
|
|
101
|
+
text-overflow: ellipsis;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.cbfk-page-actions {
|
|
105
|
+
display: inline-flex;
|
|
106
|
+
flex: none;
|
|
107
|
+
align-items: center;
|
|
108
|
+
gap: 0.5rem;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/* The Edit / Preview switch is a phone's; wide, both sides are simply there. */
|
|
112
|
+
.cbfk-page-modes {
|
|
113
|
+
display: none;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.cbfk-page-split {
|
|
117
|
+
display: flex;
|
|
118
|
+
flex: 1 1 auto;
|
|
119
|
+
min-height: 0;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.cbfk-page-form,
|
|
123
|
+
.cbfk-page-preview {
|
|
124
|
+
box-sizing: border-box;
|
|
125
|
+
min-width: 0;
|
|
126
|
+
min-height: 0;
|
|
127
|
+
/* guardrails-ignore no-raw-overflow: task 2827 — each side scrolls on its own in a Tailwind-free host, where ScrollArea cannot draw; the keyboard inset below is ScrollArea's. */
|
|
128
|
+
overflow: auto;
|
|
129
|
+
overscroll-behavior: contain;
|
|
130
|
+
/* ScrollArea's keyboard clearance (a no-op 0px on desktop): a focused field is never
|
|
131
|
+
parked under a phone's keyboard, and the last one can be scrolled above it. */
|
|
132
|
+
scroll-padding-bottom: calc(var(--keyboard-inset, 0px) + 1rem);
|
|
133
|
+
padding-bottom: var(--keyboard-inset, 0px);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.cbfk-page-form {
|
|
137
|
+
flex: 0 0 var(--cbfk-page-split, 50%);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.cbfk-page-form-inner {
|
|
141
|
+
display: flex;
|
|
142
|
+
flex-direction: column;
|
|
143
|
+
gap: 1.25rem;
|
|
144
|
+
box-sizing: border-box;
|
|
145
|
+
max-width: 46rem;
|
|
146
|
+
margin: 0 auto;
|
|
147
|
+
padding: 2rem 2rem 4rem;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.cbfk-page-preview {
|
|
151
|
+
flex: 1 1 0;
|
|
152
|
+
padding: 1.25rem 2rem 4rem;
|
|
153
|
+
background-color: var(--surface-sunken, var(--muted));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.cbfk-page-preview-head {
|
|
157
|
+
display: flex;
|
|
158
|
+
align-items: center;
|
|
159
|
+
gap: 0.75rem;
|
|
160
|
+
min-height: 2rem;
|
|
161
|
+
margin: 0 0 1rem;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.cbfk-page-preview-label {
|
|
165
|
+
flex: 1 1 auto;
|
|
166
|
+
margin: 0;
|
|
167
|
+
color: var(--muted-foreground);
|
|
168
|
+
font-size: 0.75rem;
|
|
169
|
+
font-weight: 500;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.cbfk-page-preview-tools {
|
|
173
|
+
display: inline-flex;
|
|
174
|
+
flex-wrap: wrap;
|
|
175
|
+
align-items: center;
|
|
176
|
+
gap: 0.75rem;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.cbfk-page-preview-body {
|
|
180
|
+
max-width: 40rem;
|
|
181
|
+
margin: 0 auto;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/* A 1px line with an 11px grab area either side of it; the line lights while dragged. */
|
|
185
|
+
.cbfk-page-divider {
|
|
186
|
+
position: relative;
|
|
187
|
+
flex: none;
|
|
188
|
+
width: 1px;
|
|
189
|
+
background-color: var(--border);
|
|
190
|
+
cursor: col-resize;
|
|
191
|
+
touch-action: none;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.cbfk-page-divider::before {
|
|
195
|
+
position: absolute;
|
|
196
|
+
inset: 0 -0.375rem;
|
|
197
|
+
content: "";
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
.cbfk-page-divider:hover,
|
|
201
|
+
.cbfk-page[data-dragging="true"] .cbfk-page-divider {
|
|
202
|
+
background-color: var(--ring, var(--accent));
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
.cbfk-page[data-dragging="true"] {
|
|
206
|
+
cursor: col-resize;
|
|
207
|
+
user-select: none;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/* Narrow: one side at a time, chosen in the header. */
|
|
211
|
+
@container (max-width: 48rem) {
|
|
212
|
+
/* The switch takes the title's place: Back, Edit | Preview, the actions. A title cut to
|
|
213
|
+
one letter says nothing, and the page's own heading is still its accessible name. */
|
|
214
|
+
.cbfk-page-crumb,
|
|
215
|
+
.cbfk-page-sep {
|
|
216
|
+
display: none;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
.cbfk-page-title {
|
|
220
|
+
position: absolute;
|
|
221
|
+
width: 1px;
|
|
222
|
+
height: 1px;
|
|
223
|
+
overflow: hidden;
|
|
224
|
+
clip: rect(0 0 0 0);
|
|
225
|
+
white-space: nowrap;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
.cbfk-page-modes {
|
|
229
|
+
display: inline-flex;
|
|
230
|
+
flex: none;
|
|
231
|
+
margin-right: auto;
|
|
232
|
+
padding: 0.125rem;
|
|
233
|
+
border-radius: var(--radius-sm, 0.375rem);
|
|
234
|
+
background-color: var(--muted);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
.cbfk-page-mode {
|
|
238
|
+
min-height: 2rem;
|
|
239
|
+
margin: 0;
|
|
240
|
+
padding: 0 0.75rem;
|
|
241
|
+
border: 0;
|
|
242
|
+
border-radius: calc(var(--radius-sm, 0.375rem) - 2px);
|
|
243
|
+
background: transparent;
|
|
244
|
+
color: var(--muted-foreground);
|
|
245
|
+
font: inherit;
|
|
246
|
+
font-size: 0.8125rem;
|
|
247
|
+
cursor: pointer;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
.cbfk-page-mode[aria-selected="true"] {
|
|
251
|
+
background-color: var(--background);
|
|
252
|
+
color: var(--foreground);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
.cbfk-page-divider {
|
|
256
|
+
display: none;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
.cbfk-page-form {
|
|
260
|
+
flex: 1 1 auto;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
.cbfk-page[data-mode="edit"] .cbfk-page-preview,
|
|
264
|
+
.cbfk-page[data-mode="preview"] .cbfk-page-form {
|
|
265
|
+
display: none;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
.cbfk-page-form-inner {
|
|
269
|
+
padding: 1.25rem 1rem 4rem;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
.cbfk-page-preview {
|
|
273
|
+
padding: 1rem 1rem 4rem;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/* ── IconPicker ─────────────────────────────────────────────────────────── */
|
|
279
|
+
@layer base {
|
|
280
|
+
.cbfk-icons {
|
|
281
|
+
display: flex;
|
|
282
|
+
flex-direction: column;
|
|
283
|
+
gap: 0.625rem;
|
|
284
|
+
min-width: 0;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
.cbfk-icons-sets {
|
|
288
|
+
display: flex;
|
|
289
|
+
flex-wrap: wrap;
|
|
290
|
+
gap: 0.25rem;
|
|
291
|
+
min-inline-size: 0;
|
|
292
|
+
margin: 0;
|
|
293
|
+
border: 0;
|
|
294
|
+
padding: 0;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
.cbfk-icons-set {
|
|
298
|
+
min-height: 1.75rem;
|
|
299
|
+
margin: 0;
|
|
300
|
+
padding: 0 0.625rem;
|
|
301
|
+
border: 0;
|
|
302
|
+
border-radius: 999px;
|
|
303
|
+
background: transparent;
|
|
304
|
+
color: var(--muted-foreground);
|
|
305
|
+
font: inherit;
|
|
306
|
+
font-size: 0.75rem;
|
|
307
|
+
cursor: pointer;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
.cbfk-icons-set:hover {
|
|
311
|
+
color: var(--foreground);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
.cbfk-icons-set[aria-pressed="true"] {
|
|
315
|
+
background-color: var(--muted);
|
|
316
|
+
color: var(--foreground);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
.cbfk-icons-grid {
|
|
320
|
+
display: grid;
|
|
321
|
+
grid-template-columns: repeat(auto-fill, minmax(2.5rem, 1fr));
|
|
322
|
+
gap: 0.25rem;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
.cbfk-icons-cell {
|
|
326
|
+
display: inline-flex;
|
|
327
|
+
align-items: center;
|
|
328
|
+
justify-content: center;
|
|
329
|
+
aspect-ratio: 1;
|
|
330
|
+
margin: 0;
|
|
331
|
+
padding: 0;
|
|
332
|
+
border: 1px solid transparent;
|
|
333
|
+
border-radius: var(--radius-sm, 0.375rem);
|
|
334
|
+
background: transparent;
|
|
335
|
+
cursor: pointer;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
.cbfk-icons-cell:hover {
|
|
339
|
+
background-color: var(--muted);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
.cbfk-icons-cell:focus-visible {
|
|
343
|
+
outline: 2px solid var(--ring);
|
|
344
|
+
outline-offset: -2px;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
.cbfk-icons-cell[aria-checked="true"] {
|
|
348
|
+
border-color: var(--ring, var(--accent));
|
|
349
|
+
background-color: var(--muted);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
.cbfk-icons-note {
|
|
353
|
+
margin: 0;
|
|
354
|
+
color: var(--muted-foreground);
|
|
355
|
+
font-size: 0.75rem;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/*
|
|
360
|
+
* ── QuietForm (and EditorPage's form) ──────────────────────────────────────
|
|
361
|
+
*
|
|
362
|
+
* 🔴 UNLAYERED, unlike everything above, and on purpose: a quiet form must win over the border
|
|
363
|
+
* and fill an `Input`'s own utilities draw — in `@layer utilities` in a Tailwind app, and as
|
|
364
|
+
* `.cs-root .border` (two classes) in desk's compiled copy. Only an unlayered rule out-ranks a
|
|
365
|
+
* layered one, and `:is()` takes its most specific argument (`input:not(…)`, a class and an
|
|
366
|
+
* element), so `.cbfk-quiet :is(…)` out-ranks two classes as well. It is safe because it is
|
|
367
|
+
* opt-in — it reaches only what sits inside `.cbfk-quiet`.
|
|
368
|
+
*/
|
|
369
|
+
.cbfk-quiet
|
|
370
|
+
:is(
|
|
371
|
+
input:not([type="checkbox"], [type="radio"], [type="file"], [type="range"], [type="color"]),
|
|
372
|
+
textarea,
|
|
373
|
+
select
|
|
374
|
+
) {
|
|
375
|
+
border-color: transparent;
|
|
376
|
+
background-color: transparent;
|
|
377
|
+
box-shadow: none;
|
|
378
|
+
transition:
|
|
379
|
+
border-color 120ms ease,
|
|
380
|
+
background-color 120ms ease;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
.cbfk-quiet
|
|
384
|
+
:is(
|
|
385
|
+
input:not([type="checkbox"], [type="radio"], [type="file"], [type="range"], [type="color"]),
|
|
386
|
+
textarea,
|
|
387
|
+
select
|
|
388
|
+
):is(:hover, :focus-visible, :focus) {
|
|
389
|
+
border-color: var(--border);
|
|
390
|
+
background-color: var(--background);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
.cbfk-quiet [data-cb-field-label][data-cb-field-label] {
|
|
394
|
+
color: var(--muted-foreground);
|
|
395
|
+
font-size: 0.8125rem;
|
|
396
|
+
font-weight: 450;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/* A page title in a quiet form: an input marked `data-cb-quiet-title` reads as the heading. */
|
|
400
|
+
.cbfk-quiet input[data-cb-quiet-title][data-cb-quiet-title] {
|
|
401
|
+
height: auto;
|
|
402
|
+
min-height: 2.75rem;
|
|
403
|
+
padding-inline: 0.25rem;
|
|
404
|
+
font-size: 1.5rem;
|
|
405
|
+
font-weight: 600;
|
|
406
|
+
letter-spacing: -0.01em;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/* The preview's tools are a row of small switches, not full-width settings rows. Unlayered for
|
|
410
|
+
the reason above: a `Switch` label is `w-full justify-between` in its own utilities. */
|
|
411
|
+
.cbfk-page-preview-tools > label[data-cb-tap-target] {
|
|
412
|
+
width: auto;
|
|
413
|
+
min-height: 2rem;
|
|
414
|
+
padding-block: 0;
|
|
415
|
+
gap: 0.5rem;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
.cbfk-page-preview-tools > label[data-cb-tap-target] > span:first-child {
|
|
419
|
+
color: var(--muted-foreground);
|
|
420
|
+
font-size: 0.8125rem;
|
|
421
|
+
}
|
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `cursedbelt/react/form-kit` — the builder-form pieces that draw from their own plain
|
|
3
3
|
* stylesheet (`formKit.css`), so an app with no Tailwind and no `./react` barrel (desk) can
|
|
4
|
-
* mount them. A Tailwind app gets the same
|
|
4
|
+
* mount them. A Tailwind app gets the same pieces from `cursedbelt/react`.
|
|
5
5
|
*/
|
|
6
|
+
export { EditorPage, type EditorPageProps, QuietForm, type QuietFormProps } from './EditorPage.js';
|
|
6
7
|
export { ExpandableRow, type ExpandableRowProps } from './ExpandableRow.js';
|
|
8
|
+
export {
|
|
9
|
+
ICON_SETS,
|
|
10
|
+
IconPicker,
|
|
11
|
+
type IconPickerProps,
|
|
12
|
+
type IconSet,
|
|
13
|
+
iconDataUrl,
|
|
14
|
+
type PickedIcon,
|
|
15
|
+
} from './IconPicker.js';
|
|
7
16
|
export { FormPreviewLayout, type FormPreviewLayoutProps } from './FormPreviewLayout.js';
|
|
8
17
|
export { KeyBadge, type KeyBadgeProps } from './KeyBadge.js';
|
|
9
18
|
export { RevealAction, type RevealActionProps } from './RevealAction.js';
|