@motion-proto/live-tokens 0.91.2 → 0.93.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/.claude/skills/live-tokens-create-page/SKILL.md +10 -3
- package/.claude/skills/live-tokens-set-type/SKILL.md +3 -3
- package/CHANGELOG.md +171 -0
- package/bin/lib/tokenVocabulary.mjs +2 -2
- package/bin/rules/testRuns.mjs +1 -1
- package/bin/rules/tokens.mjs +3 -3
- package/dist-plugin/chunk-7XSUHJUH.js +1652 -0
- package/dist-plugin/index.cjs +950 -15
- package/dist-plugin/index.js +1 -1
- package/dist-plugin/setType/index.cjs +23 -2
- package/dist-plugin/setType/index.js +23 -2
- package/dist-plugin/tokensCssMigrations/index.cjs +954 -15
- package/dist-plugin/tokensCssMigrations/index.d.cts +23 -3
- package/dist-plugin/tokensCssMigrations/index.d.ts +23 -3
- package/dist-plugin/tokensCssMigrations/index.js +5 -1
- package/package.json +1 -1
- package/src/app/site.css +243 -9
- package/src/demo/TestingLoops.svelte +9 -9
- package/src/editor/core/fonts/weightCoverage.ts +31 -2
- package/src/editor/core/palettes/tokenRegistry.ts +36 -37
- package/src/editor/core/store/editorKeybindings.ts +1 -1
- package/src/editor/core/themes/parsers/mediaBlocks.ts +46 -0
- package/src/editor/core/typeScale/typeScale.ts +341 -0
- package/src/editor/skill-atlas/SourcePane.svelte +3 -3
- package/src/editor/skill-atlas/skillSources.generated.ts +2 -2
- package/src/editor/skill-atlas/trees/create-page.ts +14 -14
- package/src/editor/skill-atlas/trees/set-type.ts +2 -2
- package/src/editor/ui/UIFontFamilySelector.svelte +5 -1
- package/src/editor/ui/UIFontWeightSelector.svelte +4 -0
- package/src/editor/ui/UILetterSpacingSelector.svelte +4 -0
- package/src/editor/ui/UILineHeightSelector.svelte +4 -0
- package/src/editor/ui/UITextTransformSelector.svelte +4 -0
- package/src/editor/ui/UIVariantSelector.svelte +6 -8
- package/src/editor/ui/VariablesTab.svelte +4 -4
- package/src/editor/ui/colors/ColorsTab.svelte +99 -5
- package/src/editor/ui/colors/paletteBaseColor.ts +17 -0
- package/src/editor/ui/sections/textStyles.ts +44 -92
- package/src/editor/ui/text-styles/ControlBody.svelte +100 -0
- package/src/editor/ui/text-styles/ResetIcon.svelte +60 -0
- package/src/editor/ui/text-styles/StepRow.svelte +330 -0
- package/src/editor/ui/text-styles/StyleControl.svelte +128 -0
- package/src/editor/ui/text-styles/TextStylesSection.svelte +551 -0
- package/src/editor/ui/text-styles/textStyleEdits.ts +308 -0
- package/src/editor/ui/variantScales.ts +16 -0
- package/src/system/styles/tokens.css +412 -48
- package/src/testing-js/page-compliance.contract.js +233 -80
- package/src/testing-js/page-compliance.contract.js.map +1 -1
- package/dist-plugin/chunk-GWMEYBZ2.js +0 -715
- package/src/editor/ui/TextStylesSection.svelte +0 -171
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import ControlBody from './ControlBody.svelte';
|
|
4
|
+
import ResetIcon from './ResetIcon.svelte';
|
|
5
|
+
|
|
6
|
+
interface Link {
|
|
7
|
+
linked: boolean;
|
|
8
|
+
/** What the linked value shows, such as the usage's face. */
|
|
9
|
+
text: string;
|
|
10
|
+
/** The link expression, shown as the linked value's tooltip. */
|
|
11
|
+
title: string;
|
|
12
|
+
ontoggle: () => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface Props {
|
|
16
|
+
label: string;
|
|
17
|
+
/** Names the control in accessible labels, such as `heading-xl face`. */
|
|
18
|
+
name: string;
|
|
19
|
+
width: string;
|
|
20
|
+
resetVisible: boolean;
|
|
21
|
+
onreset: () => void;
|
|
22
|
+
link?: Link;
|
|
23
|
+
children: Snippet;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let { label, name, width, resetVisible, onreset, link, children }: Props = $props();
|
|
27
|
+
</script>
|
|
28
|
+
|
|
29
|
+
<div class="ctl" data-reset-scope>
|
|
30
|
+
<div class="ctl-head">
|
|
31
|
+
<span class="ctl-label">{label}</span>
|
|
32
|
+
<ResetIcon label={name} visible={resetVisible} {onreset} />
|
|
33
|
+
</div>
|
|
34
|
+
<ControlBody {width} linked={link ? link.linked : null}>
|
|
35
|
+
{#if link?.linked}
|
|
36
|
+
<span class="linked-value" title={link.title}>{link.text}</span>
|
|
37
|
+
{:else}
|
|
38
|
+
{@render children()}
|
|
39
|
+
{/if}
|
|
40
|
+
{#snippet after()}
|
|
41
|
+
{#if link}
|
|
42
|
+
<button
|
|
43
|
+
type="button"
|
|
44
|
+
class="lock"
|
|
45
|
+
class:is-unlinked={!link.linked}
|
|
46
|
+
aria-pressed={link.linked}
|
|
47
|
+
aria-label="{link.linked ? 'Unlink' : 'Relink'} {name}"
|
|
48
|
+
title={link.linked ? 'Unlink' : 'Relink'}
|
|
49
|
+
onclick={link.ontoggle}
|
|
50
|
+
>
|
|
51
|
+
<i class="fas {link.linked ? 'fa-lock' : 'fa-lock-open'}" aria-hidden="true"></i>
|
|
52
|
+
</button>
|
|
53
|
+
{/if}
|
|
54
|
+
{/snippet}
|
|
55
|
+
</ControlBody>
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
<style>
|
|
59
|
+
.ctl {
|
|
60
|
+
display: flex;
|
|
61
|
+
flex-direction: column;
|
|
62
|
+
gap: var(--ui-space-4);
|
|
63
|
+
min-width: 0;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/* The label line has a fixed height and the reset icon trails the label, so
|
|
67
|
+
showing or hiding the icon moves nothing. */
|
|
68
|
+
.ctl-head {
|
|
69
|
+
display: flex;
|
|
70
|
+
align-items: center;
|
|
71
|
+
gap: var(--ui-space-4);
|
|
72
|
+
height: 18px;
|
|
73
|
+
min-width: 0;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.ctl-label {
|
|
77
|
+
font-size: var(--ui-font-size-xs);
|
|
78
|
+
font-weight: var(--ui-font-weight-semibold);
|
|
79
|
+
color: var(--ui-text-secondary);
|
|
80
|
+
white-space: nowrap;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.linked-value {
|
|
84
|
+
box-sizing: border-box;
|
|
85
|
+
display: block;
|
|
86
|
+
height: 1.75rem;
|
|
87
|
+
padding: 0 var(--ui-space-8);
|
|
88
|
+
background: var(--ui-surface-higher);
|
|
89
|
+
border: 1px solid var(--ui-border-low);
|
|
90
|
+
border-left: none;
|
|
91
|
+
border-radius: 0 var(--ui-radius-md) var(--ui-radius-md) 0;
|
|
92
|
+
color: var(--ui-text-secondary);
|
|
93
|
+
font-size: var(--ui-font-size-sm);
|
|
94
|
+
line-height: calc(1.75rem - 2px);
|
|
95
|
+
white-space: nowrap;
|
|
96
|
+
overflow: hidden;
|
|
97
|
+
text-overflow: ellipsis;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.lock {
|
|
101
|
+
flex: none;
|
|
102
|
+
display: inline-grid;
|
|
103
|
+
place-items: center;
|
|
104
|
+
width: 24px;
|
|
105
|
+
height: 24px;
|
|
106
|
+
padding: 0;
|
|
107
|
+
background: none;
|
|
108
|
+
border: none;
|
|
109
|
+
border-radius: var(--ui-radius-md);
|
|
110
|
+
color: var(--ui-text-tertiary);
|
|
111
|
+
font-size: 0.8125rem;
|
|
112
|
+
cursor: pointer;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.lock:hover {
|
|
116
|
+
color: var(--ui-text-primary);
|
|
117
|
+
background: var(--ui-surface-higher);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.lock:focus-visible {
|
|
121
|
+
outline: 2px solid var(--ui-text-primary);
|
|
122
|
+
outline-offset: 1px;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.lock.is-unlinked {
|
|
126
|
+
color: var(--ui-link-broken);
|
|
127
|
+
}
|
|
128
|
+
</style>
|
|
@@ -0,0 +1,551 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { tick } from 'svelte';
|
|
3
|
+
import { beginSliderGesture, editorState } from '../../core/store/editorStore';
|
|
4
|
+
import { getDeclaredValue } from '../../core/palettes/tokenRegistry';
|
|
5
|
+
import {
|
|
6
|
+
COMPRESSION_RANGE,
|
|
7
|
+
CUSTOM_RATIO_RANGE,
|
|
8
|
+
EDITABLE_NAMES,
|
|
9
|
+
INTERVALS,
|
|
10
|
+
ROOT_PX,
|
|
11
|
+
STEPS,
|
|
12
|
+
USAGES,
|
|
13
|
+
compressionName,
|
|
14
|
+
formatNumber,
|
|
15
|
+
formatRem,
|
|
16
|
+
usageName,
|
|
17
|
+
type Usage,
|
|
18
|
+
type Viewport,
|
|
19
|
+
} from '../../core/typeScale/typeScale';
|
|
20
|
+
import UIFontFamilySelector from '../UIFontFamilySelector.svelte';
|
|
21
|
+
import UIFontWeightSelector from '../UIFontWeightSelector.svelte';
|
|
22
|
+
import UIPillButton from '../UIPillButton.svelte';
|
|
23
|
+
import UISegmentedControl from '../UISegmentedControl.svelte';
|
|
24
|
+
import { TEXT_STYLES, type TextStyle } from '../sections/textStyles';
|
|
25
|
+
import ResetIcon from './ResetIcon.svelte';
|
|
26
|
+
import StepRow from './StepRow.svelte';
|
|
27
|
+
import StyleControl from './StyleControl.svelte';
|
|
28
|
+
import {
|
|
29
|
+
BASE_RANGE_PX,
|
|
30
|
+
COMPRESSED,
|
|
31
|
+
EYEBROW,
|
|
32
|
+
compressionEffect,
|
|
33
|
+
compressionOf,
|
|
34
|
+
hasStored,
|
|
35
|
+
intervalOf,
|
|
36
|
+
isStored,
|
|
37
|
+
ratioLabel,
|
|
38
|
+
settingsOf,
|
|
39
|
+
stepTarget,
|
|
40
|
+
textStyleEdits,
|
|
41
|
+
usageKeys,
|
|
42
|
+
type TokenView,
|
|
43
|
+
} from './textStyleEdits';
|
|
44
|
+
|
|
45
|
+
const edits = textStyleEdits(getDeclaredValue);
|
|
46
|
+
const uid = $props.id();
|
|
47
|
+
|
|
48
|
+
const USAGE_LABEL: Record<Usage, string> = {
|
|
49
|
+
display: 'Display',
|
|
50
|
+
heading: 'Heading',
|
|
51
|
+
body: 'Body',
|
|
52
|
+
editorial: 'Editorial',
|
|
53
|
+
code: 'Code',
|
|
54
|
+
};
|
|
55
|
+
const VIEWPORT_OPTIONS: { value: Viewport; label: string }[] = [
|
|
56
|
+
{ value: 'desktop', label: 'Desktop' },
|
|
57
|
+
{ value: 'tablet', label: 'Tablet' },
|
|
58
|
+
{ value: 'phone', label: 'Phone' },
|
|
59
|
+
];
|
|
60
|
+
const STEPS_DOWN = [...STEPS].reverse();
|
|
61
|
+
const STYLE_BY_PREFIX = new Map<string, TextStyle>(TEXT_STYLES.map((s) => [s.prefix, s]));
|
|
62
|
+
const styleOf = (prefix: string): TextStyle => STYLE_BY_PREFIX.get(prefix)!;
|
|
63
|
+
|
|
64
|
+
let active: Usage = $state('heading');
|
|
65
|
+
let viewport: Viewport = $state('desktop');
|
|
66
|
+
// A ratio that matches an interval shows that interval until the user picks Custom.
|
|
67
|
+
let customRatio: Partial<Record<Usage, boolean>> = $state({});
|
|
68
|
+
|
|
69
|
+
let view: TokenView = $derived({ stored: $editorState.cssVars, declared: getDeclaredValue });
|
|
70
|
+
let settings = $derived(settingsOf(view, active));
|
|
71
|
+
let ratioMode = $derived(customRatio[active] ? 'custom' : intervalOf(settings.ratio).key);
|
|
72
|
+
|
|
73
|
+
async function selectTab(usage: Usage) {
|
|
74
|
+
active = usage;
|
|
75
|
+
await tick();
|
|
76
|
+
document.getElementById(`${uid}-tab-${usage}`)?.focus();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const TAB_MOVES: Record<string, (i: number) => number> = {
|
|
80
|
+
ArrowRight: (i) => (i + 1) % USAGES.length,
|
|
81
|
+
ArrowLeft: (i) => (i - 1 + USAGES.length) % USAGES.length,
|
|
82
|
+
Home: () => 0,
|
|
83
|
+
End: () => USAGES.length - 1,
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
function onTabKey(e: KeyboardEvent) {
|
|
87
|
+
const move = TAB_MOVES[e.key];
|
|
88
|
+
if (!move) return;
|
|
89
|
+
e.preventDefault();
|
|
90
|
+
selectTab(USAGES[move(USAGES.indexOf(active))]);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function chooseRatio(key: string) {
|
|
94
|
+
const interval = INTERVALS.find((i) => i.key === key);
|
|
95
|
+
customRatio[active] = interval?.ratio === null;
|
|
96
|
+
if (interval?.ratio != null) edits.setRatio(active, interval.ratio);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function resetRatio() {
|
|
100
|
+
customRatio[active] = false;
|
|
101
|
+
edits.resetKey(usageName(active, 'scale-ratio'));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function resetUsage() {
|
|
105
|
+
customRatio[active] = false;
|
|
106
|
+
edits.resetUsage(active);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function resetAll() {
|
|
110
|
+
customRatio = {};
|
|
111
|
+
edits.resetAll();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// A clamped or unchanged value would otherwise stay in the input as typed.
|
|
115
|
+
function editBase(input: HTMLInputElement) {
|
|
116
|
+
edits.setBase(active, input.valueAsNumber);
|
|
117
|
+
input.value = formatNumber(settingsOf(edits.view(), active).base * ROOT_PX);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function editCustomRatio(input: HTMLInputElement) {
|
|
121
|
+
edits.setRatio(active, input.valueAsNumber);
|
|
122
|
+
input.value = formatNumber(settingsOf(edits.view(), active).ratio);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const writeSetting = (name: string) => (value: string | null) => edits.writeToken(name, value);
|
|
126
|
+
</script>
|
|
127
|
+
|
|
128
|
+
<div class="group-head">
|
|
129
|
+
<h3 class="group-title">Text Styles</h3>
|
|
130
|
+
<UIPillButton icon="fa-rotate-left" disabled={!hasStored(view, EDITABLE_NAMES)} onclick={resetAll}>
|
|
131
|
+
Reset all
|
|
132
|
+
</UIPillButton>
|
|
133
|
+
</div>
|
|
134
|
+
|
|
135
|
+
<div class="scaling">
|
|
136
|
+
<h4 class="sub-title scaling-wide">Responsive scaling</h4>
|
|
137
|
+
{#each COMPRESSED as vp (vp)}
|
|
138
|
+
{@const c = compressionOf(view, vp)}
|
|
139
|
+
{@const id = `${uid}-${vp}-scaling`}
|
|
140
|
+
<div class="scaling-field" data-reset-scope>
|
|
141
|
+
<div class="scaling-top">
|
|
142
|
+
<div class="ctl-head">
|
|
143
|
+
<label class="field-label" for={id}>{vp === 'tablet' ? 'Tablet' : 'Phone'} scaling</label>
|
|
144
|
+
<ResetIcon
|
|
145
|
+
label="{vp} scaling"
|
|
146
|
+
visible={isStored(view, compressionName(vp))}
|
|
147
|
+
onreset={() => edits.resetKey(compressionName(vp))}
|
|
148
|
+
/>
|
|
149
|
+
</div>
|
|
150
|
+
<span class="scaling-value">{c.toFixed(2)}</span>
|
|
151
|
+
</div>
|
|
152
|
+
<input
|
|
153
|
+
{id}
|
|
154
|
+
type="range"
|
|
155
|
+
min={COMPRESSION_RANGE.min}
|
|
156
|
+
max={COMPRESSION_RANGE.max}
|
|
157
|
+
step="0.01"
|
|
158
|
+
value={c}
|
|
159
|
+
onpointerdown={() => beginSliderGesture(`drag ${vp} scaling`)}
|
|
160
|
+
oninput={(e) => edits.setCompression(vp, e.currentTarget.valueAsNumber)}
|
|
161
|
+
/>
|
|
162
|
+
<span class="scaling-effect">{compressionEffect(c)}</span>
|
|
163
|
+
</div>
|
|
164
|
+
{/each}
|
|
165
|
+
<p class="hint scaling-wide">
|
|
166
|
+
Sets the recommended tablet and phone size for every step. Sizes above 16px move toward 16px.
|
|
167
|
+
Lower values fit large type to small screens and narrow the gap between levels, and higher values
|
|
168
|
+
keep more of the desktop hierarchy. Every recommendation stays at or below its desktop size.
|
|
169
|
+
</p>
|
|
170
|
+
</div>
|
|
171
|
+
|
|
172
|
+
<div>
|
|
173
|
+
<div class="tabs" role="tablist" aria-label="Usages">
|
|
174
|
+
{#each USAGES as usage (usage)}
|
|
175
|
+
{@const selected = usage === active}
|
|
176
|
+
<button
|
|
177
|
+
type="button"
|
|
178
|
+
role="tab"
|
|
179
|
+
class="tab"
|
|
180
|
+
id="{uid}-tab-{usage}"
|
|
181
|
+
aria-selected={selected}
|
|
182
|
+
aria-controls="{uid}-panel"
|
|
183
|
+
tabindex={selected ? 0 : -1}
|
|
184
|
+
onclick={() => selectTab(usage)}
|
|
185
|
+
onkeydown={onTabKey}
|
|
186
|
+
>
|
|
187
|
+
{USAGE_LABEL[usage]}
|
|
188
|
+
<span class="tab-ratio">{ratioLabel(settingsOf(view, usage).ratio)}</span>
|
|
189
|
+
</button>
|
|
190
|
+
{/each}
|
|
191
|
+
</div>
|
|
192
|
+
|
|
193
|
+
<div class="tab-panel" id="{uid}-panel" role="tabpanel" aria-labelledby="{uid}-tab-{active}">
|
|
194
|
+
{#key active}
|
|
195
|
+
<div class="scale">
|
|
196
|
+
<StyleControl
|
|
197
|
+
label="Face"
|
|
198
|
+
name="{active} face"
|
|
199
|
+
width="10rem"
|
|
200
|
+
resetVisible={isStored(view, usageName(active, 'font-family'))}
|
|
201
|
+
onreset={() => edits.resetKey(usageName(active, 'font-family'))}
|
|
202
|
+
>
|
|
203
|
+
<UIFontFamilySelector
|
|
204
|
+
variable={usageName(active, 'font-family')}
|
|
205
|
+
onwrite={writeSetting(usageName(active, 'font-family'))}
|
|
206
|
+
/>
|
|
207
|
+
</StyleControl>
|
|
208
|
+
<StyleControl
|
|
209
|
+
label="Weight"
|
|
210
|
+
name="{active} weight"
|
|
211
|
+
width="8rem"
|
|
212
|
+
resetVisible={isStored(view, usageName(active, 'font-weight'))}
|
|
213
|
+
onreset={() => edits.resetKey(usageName(active, 'font-weight'))}
|
|
214
|
+
>
|
|
215
|
+
<UIFontWeightSelector
|
|
216
|
+
variable={usageName(active, 'font-weight')}
|
|
217
|
+
onwrite={writeSetting(usageName(active, 'font-weight'))}
|
|
218
|
+
/>
|
|
219
|
+
</StyleControl>
|
|
220
|
+
<div class="field" data-reset-scope>
|
|
221
|
+
<div class="ctl-head">
|
|
222
|
+
<label class="field-label" for="{uid}-base">Base</label>
|
|
223
|
+
<ResetIcon
|
|
224
|
+
label="{active} base"
|
|
225
|
+
visible={isStored(view, usageName(active, 'scale-base'))}
|
|
226
|
+
onreset={() => edits.resetKey(usageName(active, 'scale-base'))}
|
|
227
|
+
/>
|
|
228
|
+
</div>
|
|
229
|
+
<div class="inline">
|
|
230
|
+
<input
|
|
231
|
+
id="{uid}-base"
|
|
232
|
+
class="num-input"
|
|
233
|
+
type="number"
|
|
234
|
+
min={BASE_RANGE_PX.min}
|
|
235
|
+
max={BASE_RANGE_PX.max}
|
|
236
|
+
step="any"
|
|
237
|
+
value={formatNumber(settings.base * ROOT_PX)}
|
|
238
|
+
onchange={(e) => editBase(e.currentTarget)}
|
|
239
|
+
/>
|
|
240
|
+
<span class="unit">px</span>
|
|
241
|
+
<span class="rem">{formatRem(settings.base)}</span>
|
|
242
|
+
</div>
|
|
243
|
+
</div>
|
|
244
|
+
<div class="field" data-reset-scope>
|
|
245
|
+
<div class="ctl-head">
|
|
246
|
+
<label class="field-label" for="{uid}-ratio">Ratio</label>
|
|
247
|
+
<ResetIcon
|
|
248
|
+
label="{active} ratio"
|
|
249
|
+
visible={isStored(view, usageName(active, 'scale-ratio'))}
|
|
250
|
+
onreset={resetRatio}
|
|
251
|
+
/>
|
|
252
|
+
</div>
|
|
253
|
+
<div class="inline">
|
|
254
|
+
<select
|
|
255
|
+
id="{uid}-ratio"
|
|
256
|
+
class="select-input"
|
|
257
|
+
value={ratioMode}
|
|
258
|
+
onchange={(e) => chooseRatio(e.currentTarget.value)}
|
|
259
|
+
>
|
|
260
|
+
{#each INTERVALS as interval (interval.key)}
|
|
261
|
+
<option value={interval.key}>
|
|
262
|
+
{interval.ratio === null ? interval.label : `${interval.label} · ${interval.ratio}`}
|
|
263
|
+
</option>
|
|
264
|
+
{/each}
|
|
265
|
+
</select>
|
|
266
|
+
{#if ratioMode === 'custom'}
|
|
267
|
+
<input
|
|
268
|
+
class="num-input"
|
|
269
|
+
type="number"
|
|
270
|
+
min={CUSTOM_RATIO_RANGE.min}
|
|
271
|
+
max={CUSTOM_RATIO_RANGE.max}
|
|
272
|
+
step="any"
|
|
273
|
+
value={formatNumber(settings.ratio)}
|
|
274
|
+
aria-label="{USAGE_LABEL[active]} custom ratio"
|
|
275
|
+
onchange={(e) => editCustomRatio(e.currentTarget)}
|
|
276
|
+
/>
|
|
277
|
+
{/if}
|
|
278
|
+
</div>
|
|
279
|
+
</div>
|
|
280
|
+
<p class="hint">
|
|
281
|
+
Desktop sizes follow from the base and ratio. Every step uses this face and weight until you unlink it.
|
|
282
|
+
</p>
|
|
283
|
+
<UIPillButton icon="fa-rotate-left" disabled={!hasStored(view, usageKeys(active))} onclick={resetUsage}>
|
|
284
|
+
Reset {USAGE_LABEL[active]}
|
|
285
|
+
</UIPillButton>
|
|
286
|
+
</div>
|
|
287
|
+
|
|
288
|
+
<div class="ramp-head">
|
|
289
|
+
<h4 class="sub-title">Steps</h4>
|
|
290
|
+
<UISegmentedControl bind:value={viewport} options={VIEWPORT_OPTIONS} ariaLabel="Preview viewport" />
|
|
291
|
+
</div>
|
|
292
|
+
<div class="ramp">
|
|
293
|
+
{#each STEPS_DOWN as step (step)}
|
|
294
|
+
{@const target = stepTarget(active, step)}
|
|
295
|
+
<StepRow {target} style={styleOf(target.prefix)} {view} {viewport} {edits} />
|
|
296
|
+
{/each}
|
|
297
|
+
</div>
|
|
298
|
+
{#if active === EYEBROW.usage}
|
|
299
|
+
<h4 class="sub-title">Eyebrow</h4>
|
|
300
|
+
<div class="ramp">
|
|
301
|
+
<StepRow target={EYEBROW} style={styleOf(EYEBROW.prefix)} {view} {viewport} {edits} />
|
|
302
|
+
</div>
|
|
303
|
+
{/if}
|
|
304
|
+
{/key}
|
|
305
|
+
</div>
|
|
306
|
+
</div>
|
|
307
|
+
|
|
308
|
+
<style>
|
|
309
|
+
.group-head {
|
|
310
|
+
display: flex;
|
|
311
|
+
align-items: center;
|
|
312
|
+
justify-content: space-between;
|
|
313
|
+
flex-wrap: wrap;
|
|
314
|
+
gap: var(--ui-space-12);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
.group-title {
|
|
318
|
+
font-size: var(--ui-font-size-xl);
|
|
319
|
+
font-weight: var(--ui-font-weight-bold);
|
|
320
|
+
color: var(--ui-text-primary);
|
|
321
|
+
margin: 0;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
.sub-title {
|
|
325
|
+
margin: 0;
|
|
326
|
+
font-size: var(--ui-font-size-md);
|
|
327
|
+
font-weight: var(--ui-font-weight-semibold);
|
|
328
|
+
color: var(--ui-text-primary);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
.scaling {
|
|
332
|
+
display: flex;
|
|
333
|
+
flex-wrap: wrap;
|
|
334
|
+
gap: var(--ui-space-10) var(--ui-space-24);
|
|
335
|
+
padding: var(--ui-space-12) var(--ui-space-16);
|
|
336
|
+
background: var(--ui-surface-low);
|
|
337
|
+
border: 1px solid var(--ui-border-low);
|
|
338
|
+
border-radius: var(--ui-radius-lg);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
.scaling-wide {
|
|
342
|
+
flex: 1 1 100%;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
.scaling-field {
|
|
346
|
+
flex: 1 1 16rem;
|
|
347
|
+
display: grid;
|
|
348
|
+
gap: var(--ui-space-4);
|
|
349
|
+
min-width: 0;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
.scaling-top {
|
|
353
|
+
display: flex;
|
|
354
|
+
justify-content: space-between;
|
|
355
|
+
align-items: center;
|
|
356
|
+
gap: var(--ui-space-8);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
.scaling-value {
|
|
360
|
+
color: var(--ui-text-primary);
|
|
361
|
+
font-variant-numeric: tabular-nums;
|
|
362
|
+
font-weight: var(--ui-font-weight-semibold);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
.scaling-field input[type="range"] {
|
|
366
|
+
width: 100%;
|
|
367
|
+
margin: 0;
|
|
368
|
+
accent-color: var(--ui-text-primary);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
.scaling-effect {
|
|
372
|
+
color: var(--ui-text-tertiary);
|
|
373
|
+
font-size: var(--ui-font-size-xs);
|
|
374
|
+
font-variant-numeric: tabular-nums;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/* The active tab carries the bright underline the editor gives section
|
|
378
|
+
titles, sitting on the bar's own hairline. */
|
|
379
|
+
.tabs {
|
|
380
|
+
display: flex;
|
|
381
|
+
flex-wrap: wrap;
|
|
382
|
+
border-bottom: 1px solid var(--ui-border-low);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
.tab {
|
|
386
|
+
display: flex;
|
|
387
|
+
flex-direction: column;
|
|
388
|
+
align-items: flex-start;
|
|
389
|
+
gap: var(--ui-space-2);
|
|
390
|
+
margin-bottom: -1px;
|
|
391
|
+
padding: var(--ui-space-10) var(--ui-space-20) var(--ui-space-8);
|
|
392
|
+
background: none;
|
|
393
|
+
border: none;
|
|
394
|
+
border-bottom: 2px solid transparent;
|
|
395
|
+
color: var(--ui-text-tertiary);
|
|
396
|
+
font: inherit;
|
|
397
|
+
font-size: var(--ui-font-size-md);
|
|
398
|
+
font-weight: var(--ui-font-weight-semibold);
|
|
399
|
+
text-align: left;
|
|
400
|
+
cursor: pointer;
|
|
401
|
+
transition:
|
|
402
|
+
color var(--ui-transition-fast),
|
|
403
|
+
border-color var(--ui-transition-fast);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
.tab:hover {
|
|
407
|
+
color: var(--ui-text-primary);
|
|
408
|
+
border-bottom-color: var(--ui-border-high);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
.tab[aria-selected="true"] {
|
|
412
|
+
color: var(--ui-text-primary);
|
|
413
|
+
border-bottom-color: var(--ui-text-primary);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
.tab:focus-visible {
|
|
417
|
+
outline: 2px solid var(--ui-text-primary);
|
|
418
|
+
outline-offset: -2px;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
.tab-ratio {
|
|
422
|
+
color: var(--ui-text-muted);
|
|
423
|
+
font-size: var(--ui-font-size-xs);
|
|
424
|
+
font-weight: var(--ui-font-weight-normal);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
.tab[aria-selected="true"] .tab-ratio {
|
|
428
|
+
color: var(--ui-text-secondary);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
.tab-panel {
|
|
432
|
+
display: flex;
|
|
433
|
+
flex-direction: column;
|
|
434
|
+
gap: var(--ui-space-16);
|
|
435
|
+
padding-top: var(--ui-space-16);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
.scale {
|
|
439
|
+
display: flex;
|
|
440
|
+
flex-wrap: wrap;
|
|
441
|
+
align-items: flex-end;
|
|
442
|
+
gap: var(--ui-space-16);
|
|
443
|
+
padding: var(--ui-space-16);
|
|
444
|
+
background: var(--ui-surface-low);
|
|
445
|
+
border: 1px solid var(--ui-border-low);
|
|
446
|
+
border-radius: var(--ui-radius-lg);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
.field {
|
|
450
|
+
display: flex;
|
|
451
|
+
flex-direction: column;
|
|
452
|
+
gap: var(--ui-space-4);
|
|
453
|
+
min-width: 0;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
.ctl-head {
|
|
457
|
+
display: flex;
|
|
458
|
+
align-items: center;
|
|
459
|
+
gap: var(--ui-space-4);
|
|
460
|
+
height: 18px;
|
|
461
|
+
min-width: 0;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
.field-label {
|
|
465
|
+
font-size: var(--ui-font-size-xs);
|
|
466
|
+
font-weight: var(--ui-font-weight-semibold);
|
|
467
|
+
color: var(--ui-text-secondary);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
.inline {
|
|
471
|
+
display: flex;
|
|
472
|
+
align-items: center;
|
|
473
|
+
gap: var(--ui-space-6);
|
|
474
|
+
min-width: 0;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
.num-input,
|
|
478
|
+
.select-input {
|
|
479
|
+
box-sizing: border-box;
|
|
480
|
+
height: 1.75rem;
|
|
481
|
+
min-width: 0;
|
|
482
|
+
padding: 0 var(--ui-space-8);
|
|
483
|
+
background: var(--ui-surface-high);
|
|
484
|
+
border: 1px solid var(--ui-border-low);
|
|
485
|
+
border-radius: var(--ui-radius-md);
|
|
486
|
+
color: var(--ui-text-primary);
|
|
487
|
+
font: inherit;
|
|
488
|
+
font-size: var(--ui-font-size-sm);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
.num-input {
|
|
492
|
+
width: 5.5rem;
|
|
493
|
+
font-variant-numeric: tabular-nums;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
.num-input:hover,
|
|
497
|
+
.select-input:hover {
|
|
498
|
+
border-color: var(--ui-border-high);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
.num-input:focus-visible,
|
|
502
|
+
.select-input:focus-visible,
|
|
503
|
+
.scaling-field input[type="range"]:focus-visible {
|
|
504
|
+
outline: 2px solid var(--ui-text-primary);
|
|
505
|
+
outline-offset: 2px;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
.unit {
|
|
509
|
+
flex: none;
|
|
510
|
+
color: var(--ui-text-muted);
|
|
511
|
+
font-size: var(--ui-font-size-xs);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
.rem {
|
|
515
|
+
flex: none;
|
|
516
|
+
width: 9ch;
|
|
517
|
+
color: var(--ui-text-muted);
|
|
518
|
+
font-family: var(--ui-font-mono);
|
|
519
|
+
font-size: var(--ui-font-size-xs);
|
|
520
|
+
font-variant-numeric: tabular-nums;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
.hint {
|
|
524
|
+
flex: 1 1 14rem;
|
|
525
|
+
margin: 0;
|
|
526
|
+
color: var(--ui-text-muted);
|
|
527
|
+
font-size: var(--ui-font-size-xs);
|
|
528
|
+
line-height: var(--ui-line-height-relaxed);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
.ramp-head {
|
|
532
|
+
display: flex;
|
|
533
|
+
align-items: center;
|
|
534
|
+
justify-content: space-between;
|
|
535
|
+
flex-wrap: wrap;
|
|
536
|
+
gap: var(--ui-space-12);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/* No overflow clipping: the pickers' dropdowns spill past the frame. */
|
|
540
|
+
.ramp {
|
|
541
|
+
display: flex;
|
|
542
|
+
flex-direction: column;
|
|
543
|
+
border: 1px solid var(--ui-border);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
@media (prefers-reduced-motion: reduce) {
|
|
547
|
+
.tab {
|
|
548
|
+
transition: none;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
</style>
|