@agent-native/core 0.80.9 → 0.80.10
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/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +7 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/cli/design-connect.ts +28 -2
- package/corpus/core/src/cli/skills.ts +45 -24
- package/corpus/templates/design/DESIGN-STUDIO-PLAN.md +717 -0
- package/corpus/templates/design/actions/add-breakpoint.ts +143 -0
- package/corpus/templates/design/actions/add-localhost-screens.ts +5 -0
- package/corpus/templates/design/actions/apply-a11y-fix.ts +317 -0
- package/corpus/templates/design/actions/apply-component-prop-edit.ts +433 -0
- package/corpus/templates/design/actions/apply-design-state.ts +200 -0
- package/corpus/templates/design/actions/apply-design-token-edit.ts +202 -0
- package/corpus/templates/design/actions/apply-motion-edit.ts +450 -0
- package/corpus/templates/design/actions/apply-shader-fill.ts +404 -0
- package/corpus/templates/design/actions/apply-visual-edit.ts +191 -5
- package/corpus/templates/design/actions/capture-design-state.ts +224 -0
- package/corpus/templates/design/actions/connect-builder-app.ts +162 -0
- package/corpus/templates/design/actions/create-component.ts +447 -0
- package/corpus/templates/design/actions/create-design-branch.ts +263 -0
- package/corpus/templates/design/actions/create-design-state.ts +162 -0
- package/corpus/templates/design/actions/delete-design-state.ts +55 -0
- package/corpus/templates/design/actions/deploy-design-preview.ts +275 -0
- package/corpus/templates/design/actions/get-component-details.ts +242 -0
- package/corpus/templates/design/actions/get-design-branch-diff.ts +362 -0
- package/corpus/templates/design/actions/get-design-review.ts +314 -0
- package/corpus/templates/design/actions/get-design-surface-index.ts +582 -0
- package/corpus/templates/design/actions/get-motion-timeline.ts +99 -0
- package/corpus/templates/design/actions/index-components.ts +258 -0
- package/corpus/templates/design/actions/index-design-tokens.ts +273 -0
- package/corpus/templates/design/actions/list-design-extensions.ts +309 -0
- package/corpus/templates/design/actions/list-design-source-capabilities.ts +153 -0
- package/corpus/templates/design/actions/list-design-states.ts +72 -0
- package/corpus/templates/design/actions/migrate-inline-design-to-app.ts +298 -0
- package/corpus/templates/design/actions/open-component-source.ts +237 -0
- package/corpus/templates/design/actions/preview-component-prop-edit.ts +254 -0
- package/corpus/templates/design/actions/preview-design-token-edit.ts +113 -0
- package/corpus/templates/design/actions/preview-shader-fill.ts +189 -0
- package/corpus/templates/design/actions/remove-breakpoint.ts +103 -0
- package/corpus/templates/design/actions/remove-motion-timeline.ts +196 -0
- package/corpus/templates/design/actions/run-design-audit.ts +421 -0
- package/corpus/templates/design/actions/run-design-extension-action.ts +284 -0
- package/corpus/templates/design/actions/set-active-breakpoint.ts +39 -0
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +1414 -71
- package/corpus/templates/design/app/components/design/DesignExtensionsPanel.tsx +632 -43
- package/corpus/templates/design/app/components/design/DrawOverlay.tsx +1 -1
- package/corpus/templates/design/app/components/design/EditPanel.tsx +1391 -52
- package/corpus/templates/design/app/components/design/LayersPanel.tsx +4 -3
- package/corpus/templates/design/app/components/design/LocalSourceEditBanner.tsx +157 -0
- package/corpus/templates/design/app/components/design/MotionDock.tsx +1062 -0
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +1052 -98
- package/corpus/templates/design/app/components/design/ReviewPanel.tsx +759 -0
- package/corpus/templates/design/app/components/design/StatesPanel.tsx +579 -0
- package/corpus/templates/design/app/components/design/TokensPanel.tsx +573 -0
- package/corpus/templates/design/app/components/design/canvas-primitive-style.ts +241 -0
- package/corpus/templates/design/app/components/design/index.ts +16 -0
- package/corpus/templates/design/app/components/design/inspector/AutoLayoutMatrix.tsx +15 -6
- package/corpus/templates/design/app/components/design/inspector/DesignColorPicker.tsx +66 -3
- package/corpus/templates/design/app/components/design/inspector/InspectorAiActions.tsx +145 -0
- package/corpus/templates/design/app/components/design/inspector/SHADER_INTEGRATION.md +42 -12
- package/corpus/templates/design/app/components/visual-editor/DrawOverlay.tsx +1 -1
- package/corpus/templates/design/app/hooks/useAgentEditRequest.ts +131 -0
- package/corpus/templates/design/app/hooks/useDesignHotkeys.ts +3 -1
- package/corpus/templates/design/app/i18n/zh-TW.ts +31 -0
- package/corpus/templates/design/app/i18n-data.ts +269 -0
- package/corpus/templates/design/app/pages/DesignEditor.tsx +2478 -386
- package/corpus/templates/design/changelog/2026-06-29-added-a-review-panel-in-the-design-editor-s-inspector-with-a.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-copying-and-pasting-layers-no-longer-shows-success-notificat.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-device-presets-in-all-screens-view-resize-the-selected-previ.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-layer-and-canvas-drags-keep-the-layer-list-stable-while-chan.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-layer-moves-can-be-undone-and-redone-without-flashing-the-ca.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-screen-previews-use-a-single-blue-hover-border-in-all-screen.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-selected-containers-show-draggable-padding-and-gap-guides-on.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-the-design-editor-adds-a-studio-layer-with-tokens-respon.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-accessibility-findings-now-offer-a-one-click-fix.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-component-instances-now-have-editable-props-in-the-inspe.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-inspect-code-now-shows-the-elements-opening-tag-at-a-gla.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-shader-fill-presets-can-now-be-applied-to-an-element.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-the-motion-timeline-can-now-add-a-track-to-any-element-a.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-visual-editor-selection-layer-paint-and-drawing-controls-are-more-reliable.md +6 -0
- package/corpus/templates/design/e2e/helpers.ts +10 -4
- package/corpus/templates/design/server/db/schema.ts +123 -0
- package/corpus/templates/design/server/plugins/db.ts +90 -0
- package/corpus/templates/design/shared/builder-app.ts +297 -0
- package/corpus/templates/design/shared/capability-resolver.ts +123 -0
- package/corpus/templates/design/shared/capture-sanitize.ts +70 -0
- package/corpus/templates/design/shared/code-layer.ts +1016 -71
- package/corpus/templates/design/shared/component-model.ts +239 -0
- package/corpus/templates/design/shared/design-review.ts +275 -0
- package/corpus/templates/design/shared/design-source-capabilities.ts +286 -0
- package/corpus/templates/design/shared/design-state.ts +112 -0
- package/corpus/templates/design/shared/design-surface-index.ts +258 -0
- package/corpus/templates/design/shared/motion-compiler.ts +278 -0
- package/corpus/templates/design/shared/motion-timeline.ts +193 -0
- package/corpus/templates/design/shared/responsive-classes.ts +452 -0
- package/corpus/templates/design/shared/shader-fill.ts +323 -0
- package/corpus/templates/design/shared/source-mode.ts +245 -0
- package/corpus/templates/plan/app/components/plan/CanvasArea.tsx +6 -2
- package/corpus/templates/plan/changelog/2026-06-29-plan-canvases-open-without-an-initial-pan-and-zoom-flicker.md +6 -0
- package/dist/cli/design-connect.d.ts.map +1 -1
- package/dist/cli/design-connect.js +28 -2
- package/dist/cli/design-connect.js.map +1 -1
- package/dist/cli/skills.d.ts.map +1 -1
- package/dist/cli/skills.js +37 -21
- package/dist/cli/skills.js.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +2 -2
- package/dist/notifications/routes.d.ts +2 -2
- package/dist/observability/routes.d.ts +7 -7
- package/dist/resources/handlers.d.ts +2 -2
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Responsive-aware Tailwind class model for the Design Studio (§6.4).
|
|
3
|
+
*
|
|
4
|
+
* The canvas is mobile-first: the base (unprefixed) class is what renders on
|
|
5
|
+
* the narrowest breakpoint; each larger breakpoint layers overrides upward,
|
|
6
|
+
* matching Tailwind's min-width cascade.
|
|
7
|
+
*
|
|
8
|
+
* Frame widths snap to Tailwind's canonical breakpoint thresholds:
|
|
9
|
+
* < 640 → base (unprefixed)
|
|
10
|
+
* 640–767 → sm:
|
|
11
|
+
* 768–1023 → md:
|
|
12
|
+
* 1024–1279 → lg:
|
|
13
|
+
* 1280–1535 → xl:
|
|
14
|
+
* ≥ 1536 → 2xl:
|
|
15
|
+
*
|
|
16
|
+
* All helpers are pure (no DB, no DOM, no side-effects).
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import type { TailwindBreakpointPrefix } from "./design-state.js";
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Types
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* A single Tailwind class token split into its constituent parts.
|
|
27
|
+
* `prefix` is `"base"` for unprefixed tokens.
|
|
28
|
+
*/
|
|
29
|
+
export interface ParsedClassToken {
|
|
30
|
+
/** Original token string as it appears in the className (e.g. "md:text-lg"). */
|
|
31
|
+
raw: string;
|
|
32
|
+
/** Breakpoint scope. `"base"` means the token has no responsive prefix. */
|
|
33
|
+
prefix: TailwindBreakpointPrefix;
|
|
34
|
+
/**
|
|
35
|
+
* The utility after the prefix (e.g. "text-lg" from "md:text-lg").
|
|
36
|
+
* For base tokens this equals `raw`.
|
|
37
|
+
*/
|
|
38
|
+
utility: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The className string parsed into per-breakpoint groups.
|
|
43
|
+
* Each group preserves the order classes appeared in the source string.
|
|
44
|
+
*/
|
|
45
|
+
export type BreakpointClassGroups = {
|
|
46
|
+
[P in TailwindBreakpointPrefix]: string[];
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
// Constants
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
/** Ordered list of Tailwind min-width thresholds in pixels. */
|
|
54
|
+
const BREAKPOINT_MIN_WIDTHS: ReadonlyArray<{
|
|
55
|
+
prefix: TailwindBreakpointPrefix;
|
|
56
|
+
minPx: number;
|
|
57
|
+
}> = [
|
|
58
|
+
{ prefix: "2xl", minPx: 1536 },
|
|
59
|
+
{ prefix: "xl", minPx: 1280 },
|
|
60
|
+
{ prefix: "lg", minPx: 1024 },
|
|
61
|
+
{ prefix: "md", minPx: 768 },
|
|
62
|
+
{ prefix: "sm", minPx: 640 },
|
|
63
|
+
// base has no minimum — it is the fallback below sm.
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
const ALL_PREFIXES: ReadonlyArray<TailwindBreakpointPrefix> = [
|
|
67
|
+
"base",
|
|
68
|
+
"sm",
|
|
69
|
+
"md",
|
|
70
|
+
"lg",
|
|
71
|
+
"xl",
|
|
72
|
+
"2xl",
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
/** Regex that matches a responsive prefix at the start of a class token. */
|
|
76
|
+
const PREFIX_RE = /^(2xl|xl|lg|md|sm):/;
|
|
77
|
+
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
// Parsing
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Parse a single class token (e.g. `"md:text-lg"`) into its parts.
|
|
84
|
+
* Handles arbitrary-value utilities like `"lg:w-[calc(100%-1rem)]"`.
|
|
85
|
+
*/
|
|
86
|
+
export function parseClassToken(token: string): ParsedClassToken {
|
|
87
|
+
const match = PREFIX_RE.exec(token);
|
|
88
|
+
if (!match) {
|
|
89
|
+
return { raw: token, prefix: "base", utility: token };
|
|
90
|
+
}
|
|
91
|
+
const prefix = match[1] as TailwindBreakpointPrefix;
|
|
92
|
+
const utility = token.slice(match[0].length);
|
|
93
|
+
return { raw: token, prefix, utility };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Split a `className` string into individual tokens and group them by
|
|
98
|
+
* breakpoint prefix. Whitespace is normalised; duplicates are preserved in
|
|
99
|
+
* the order they appear.
|
|
100
|
+
*
|
|
101
|
+
* @example
|
|
102
|
+
* parseClassGroups("text-sm md:text-base lg:text-lg")
|
|
103
|
+
* // → { base: ["text-sm"], sm: [], md: ["text-base"], lg: ["text-lg"], xl: [], "2xl": [] }
|
|
104
|
+
*/
|
|
105
|
+
export function parseClassGroups(className: string): BreakpointClassGroups {
|
|
106
|
+
const groups: BreakpointClassGroups = {
|
|
107
|
+
base: [],
|
|
108
|
+
sm: [],
|
|
109
|
+
md: [],
|
|
110
|
+
lg: [],
|
|
111
|
+
xl: [],
|
|
112
|
+
"2xl": [],
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const tokens = className.trim().split(/\s+/).filter(Boolean);
|
|
116
|
+
for (const token of tokens) {
|
|
117
|
+
const { prefix } = parseClassToken(token);
|
|
118
|
+
groups[prefix].push(token);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return groups;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
// Property extraction
|
|
126
|
+
// ---------------------------------------------------------------------------
|
|
127
|
+
|
|
128
|
+
/** Single-word utilities whose CSS property is fixed regardless of suffix. */
|
|
129
|
+
const SINGLE_WORD_PROPERTY: Readonly<Record<string, string>> = {
|
|
130
|
+
flex: "display",
|
|
131
|
+
grid: "display",
|
|
132
|
+
block: "display",
|
|
133
|
+
inline: "display",
|
|
134
|
+
"inline-block": "display",
|
|
135
|
+
"inline-flex": "display",
|
|
136
|
+
"inline-grid": "display",
|
|
137
|
+
hidden: "display",
|
|
138
|
+
contents: "display",
|
|
139
|
+
"flow-root": "display",
|
|
140
|
+
table: "display",
|
|
141
|
+
"list-item": "display",
|
|
142
|
+
static: "position",
|
|
143
|
+
fixed: "position",
|
|
144
|
+
absolute: "position",
|
|
145
|
+
relative: "position",
|
|
146
|
+
sticky: "position",
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const TEXT_ALIGN = new Set([
|
|
150
|
+
"left",
|
|
151
|
+
"center",
|
|
152
|
+
"right",
|
|
153
|
+
"justify",
|
|
154
|
+
"start",
|
|
155
|
+
"end",
|
|
156
|
+
]);
|
|
157
|
+
const FONT_SIZES = new Set([
|
|
158
|
+
"xs",
|
|
159
|
+
"sm",
|
|
160
|
+
"base",
|
|
161
|
+
"lg",
|
|
162
|
+
"xl",
|
|
163
|
+
"2xl",
|
|
164
|
+
"3xl",
|
|
165
|
+
"4xl",
|
|
166
|
+
"5xl",
|
|
167
|
+
"6xl",
|
|
168
|
+
"7xl",
|
|
169
|
+
"8xl",
|
|
170
|
+
"9xl",
|
|
171
|
+
]);
|
|
172
|
+
const TEXT_OVERFLOW = new Set(["ellipsis", "clip"]);
|
|
173
|
+
const TEXT_WRAP = new Set(["wrap", "nowrap", "balance", "pretty"]);
|
|
174
|
+
const FONT_WEIGHTS = new Set([
|
|
175
|
+
"thin",
|
|
176
|
+
"extralight",
|
|
177
|
+
"light",
|
|
178
|
+
"normal",
|
|
179
|
+
"medium",
|
|
180
|
+
"semibold",
|
|
181
|
+
"bold",
|
|
182
|
+
"extrabold",
|
|
183
|
+
"black",
|
|
184
|
+
]);
|
|
185
|
+
const AXIS_FAMILIES = new Set([
|
|
186
|
+
"min",
|
|
187
|
+
"max",
|
|
188
|
+
"space",
|
|
189
|
+
"gap",
|
|
190
|
+
"divide",
|
|
191
|
+
"scroll",
|
|
192
|
+
"inset",
|
|
193
|
+
]);
|
|
194
|
+
const LENGTH_RE = /\d|rem|em|px|%|ch|vw|vh|vmin|vmax/;
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Map a Tailwind utility to a stable CSS-property key so two utilities that
|
|
198
|
+
* target the SAME property compare equal (and replace one another rather than
|
|
199
|
+
* accumulate), while utilities for DIFFERENT properties stay distinct.
|
|
200
|
+
*
|
|
201
|
+
* Heuristic, not a full Tailwind AST — but it resolves the families where a
|
|
202
|
+
* naive "leading segment" key collides: the single-word `display`/`position`
|
|
203
|
+
* families; `text-*` (align vs size vs color vs overflow vs wrap); `font-*`
|
|
204
|
+
* (weight vs family); `bg-*` (color vs image vs size vs repeat vs ...); the
|
|
205
|
+
* flex/grid alignment families; and axis-split families (`min-w` ≠ `min-h`,
|
|
206
|
+
* `gap-x` ≠ `gap-y`). Everything else falls back to the leading segment, which
|
|
207
|
+
* is correct for `w-`, `h-`, `p-`, `m-`, `rounded-`, `border-`, etc.
|
|
208
|
+
*
|
|
209
|
+
* Exported so other callers (e.g. apply-visual-edit) derive the SAME key and
|
|
210
|
+
* stay consistent with set/remove here.
|
|
211
|
+
*/
|
|
212
|
+
export function utilityStem(utility: string): string {
|
|
213
|
+
const u = utility.startsWith("-") ? utility.slice(1) : utility;
|
|
214
|
+
const bracketIdx = u.indexOf("[");
|
|
215
|
+
const head = bracketIdx >= 0 ? u.slice(0, bracketIdx).replace(/-$/, "") : u;
|
|
216
|
+
const arbitrary = bracketIdx >= 0 ? u.slice(bracketIdx + 1) : "";
|
|
217
|
+
|
|
218
|
+
if (head in SINGLE_WORD_PROPERTY) return SINGLE_WORD_PROPERTY[head];
|
|
219
|
+
|
|
220
|
+
const seg = head.split("-");
|
|
221
|
+
const family = seg[0];
|
|
222
|
+
const rest = seg.slice(1).join("-");
|
|
223
|
+
|
|
224
|
+
if (family === "text") {
|
|
225
|
+
if (TEXT_ALIGN.has(rest)) return "text-align";
|
|
226
|
+
if (FONT_SIZES.has(rest)) return "font-size";
|
|
227
|
+
if (TEXT_OVERFLOW.has(rest)) return "text-overflow";
|
|
228
|
+
if (TEXT_WRAP.has(rest)) return "text-wrap";
|
|
229
|
+
if (bracketIdx >= 0) {
|
|
230
|
+
return LENGTH_RE.test(arbitrary) ? "font-size" : "text-color";
|
|
231
|
+
}
|
|
232
|
+
return "text-color";
|
|
233
|
+
}
|
|
234
|
+
if (family === "font") {
|
|
235
|
+
return FONT_WEIGHTS.has(rest) ? "font-weight" : "font-family";
|
|
236
|
+
}
|
|
237
|
+
if (family === "bg") {
|
|
238
|
+
if (rest === "none" || rest.startsWith("gradient"))
|
|
239
|
+
return "background-image";
|
|
240
|
+
if (rest === "auto" || rest === "cover" || rest === "contain") {
|
|
241
|
+
return "background-size";
|
|
242
|
+
}
|
|
243
|
+
if (
|
|
244
|
+
rest === "no-repeat" ||
|
|
245
|
+
rest === "repeat" ||
|
|
246
|
+
rest.startsWith("repeat-")
|
|
247
|
+
) {
|
|
248
|
+
return "background-repeat";
|
|
249
|
+
}
|
|
250
|
+
if (rest === "fixed" || rest === "local" || rest === "scroll") {
|
|
251
|
+
return "background-attachment";
|
|
252
|
+
}
|
|
253
|
+
if (rest.startsWith("clip-")) return "background-clip";
|
|
254
|
+
if (rest.startsWith("origin-")) return "background-origin";
|
|
255
|
+
if (rest.startsWith("blend-")) return "background-blend-mode";
|
|
256
|
+
if (
|
|
257
|
+
rest === "bottom" ||
|
|
258
|
+
rest === "center" ||
|
|
259
|
+
rest === "top" ||
|
|
260
|
+
rest.startsWith("left") ||
|
|
261
|
+
rest.startsWith("right")
|
|
262
|
+
) {
|
|
263
|
+
return "background-position";
|
|
264
|
+
}
|
|
265
|
+
return "background-color";
|
|
266
|
+
}
|
|
267
|
+
if (family === "justify") {
|
|
268
|
+
return rest.startsWith("items")
|
|
269
|
+
? "justify-items"
|
|
270
|
+
: rest.startsWith("self")
|
|
271
|
+
? "justify-self"
|
|
272
|
+
: "justify-content";
|
|
273
|
+
}
|
|
274
|
+
if (family === "items") return "align-items";
|
|
275
|
+
if (family === "self") return "align-self";
|
|
276
|
+
if (family === "content") return "align-content";
|
|
277
|
+
|
|
278
|
+
if (AXIS_FAMILIES.has(family) && seg.length >= 2) {
|
|
279
|
+
return `${family}-${seg[1]}`;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return family;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// ---------------------------------------------------------------------------
|
|
286
|
+
// Get / set per-breakpoint
|
|
287
|
+
// ---------------------------------------------------------------------------
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Return all tokens in `className` whose prefix equals `prefix` and whose
|
|
291
|
+
* utility starts with `stem` (the CSS property stem, e.g. `"text"` to find
|
|
292
|
+
* `"text-sm"`, `"md:text-base"`, etc.).
|
|
293
|
+
*
|
|
294
|
+
* @example
|
|
295
|
+
* getPropertyClasses("text-sm md:text-base", "md", "text")
|
|
296
|
+
* // → ["md:text-base"]
|
|
297
|
+
*/
|
|
298
|
+
export function getPropertyClasses(
|
|
299
|
+
className: string,
|
|
300
|
+
prefix: TailwindBreakpointPrefix,
|
|
301
|
+
stem: string,
|
|
302
|
+
): string[] {
|
|
303
|
+
const groups = parseClassGroups(className);
|
|
304
|
+
return groups[prefix].filter((tok) => {
|
|
305
|
+
const { utility } = parseClassToken(tok);
|
|
306
|
+
return utilityStem(utility) === stem;
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Set the class for `property` at `prefix` to `newUtility`, returning the
|
|
312
|
+
* updated className string.
|
|
313
|
+
*
|
|
314
|
+
* - If a class with the same stem already exists at this prefix it is replaced.
|
|
315
|
+
* - If no such class exists the new one is appended.
|
|
316
|
+
* - Classes at all other prefixes are left untouched.
|
|
317
|
+
* - `newUtility` should be the bare utility without the prefix (e.g. `"text-lg"`
|
|
318
|
+
* not `"md:text-lg"`); the prefix is added automatically.
|
|
319
|
+
*
|
|
320
|
+
* @example
|
|
321
|
+
* setPropertyClass("text-sm md:text-base", "lg", "text-xl")
|
|
322
|
+
* // → "text-sm md:text-base lg:text-xl"
|
|
323
|
+
*
|
|
324
|
+
* setPropertyClass("text-sm md:text-base md:font-bold", "md", "text-lg")
|
|
325
|
+
* // → "text-sm md:text-lg md:font-bold"
|
|
326
|
+
*/
|
|
327
|
+
export function setPropertyClass(
|
|
328
|
+
className: string,
|
|
329
|
+
prefix: TailwindBreakpointPrefix,
|
|
330
|
+
newUtility: string,
|
|
331
|
+
): string {
|
|
332
|
+
const stem = utilityStem(newUtility);
|
|
333
|
+
const newToken = prefix === "base" ? newUtility : `${prefix}:${newUtility}`;
|
|
334
|
+
|
|
335
|
+
const tokens = className.trim().split(/\s+/).filter(Boolean);
|
|
336
|
+
let replaced = false;
|
|
337
|
+
const next: string[] = [];
|
|
338
|
+
|
|
339
|
+
for (const token of tokens) {
|
|
340
|
+
const parsed = parseClassToken(token);
|
|
341
|
+
if (parsed.prefix === prefix && utilityStem(parsed.utility) === stem) {
|
|
342
|
+
if (!replaced) {
|
|
343
|
+
next.push(newToken);
|
|
344
|
+
replaced = true;
|
|
345
|
+
}
|
|
346
|
+
// drop duplicate occurrences of the same stem at the same prefix
|
|
347
|
+
} else {
|
|
348
|
+
next.push(token);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (!replaced) {
|
|
353
|
+
next.push(newToken);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return next.join(" ");
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Remove all tokens whose prefix equals `prefix` and whose utility stem matches
|
|
361
|
+
* `stem`. Returns the updated className string.
|
|
362
|
+
*
|
|
363
|
+
* @example
|
|
364
|
+
* removePropertyClass("text-sm md:text-base lg:text-lg", "md", "text")
|
|
365
|
+
* // → "text-sm lg:text-lg"
|
|
366
|
+
*/
|
|
367
|
+
export function removePropertyClass(
|
|
368
|
+
className: string,
|
|
369
|
+
prefix: TailwindBreakpointPrefix,
|
|
370
|
+
stem: string,
|
|
371
|
+
): string {
|
|
372
|
+
const tokens = className.trim().split(/\s+/).filter(Boolean);
|
|
373
|
+
return tokens
|
|
374
|
+
.filter((token) => {
|
|
375
|
+
const parsed = parseClassToken(token);
|
|
376
|
+
return !(
|
|
377
|
+
parsed.prefix === prefix && utilityStem(parsed.utility) === stem
|
|
378
|
+
);
|
|
379
|
+
})
|
|
380
|
+
.join(" ");
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// ---------------------------------------------------------------------------
|
|
384
|
+
// Frame-width → prefix mapping
|
|
385
|
+
// ---------------------------------------------------------------------------
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Given the pixel width of a canvas frame, return the Tailwind responsive
|
|
389
|
+
* prefix that should be written when editing a layer in that frame.
|
|
390
|
+
*
|
|
391
|
+
* - Mobile-first: a frame below `sm`'s min-width (640 px) writes `"base"`.
|
|
392
|
+
* - Each larger frame writes the prefix whose min-width that frame satisfies.
|
|
393
|
+
*
|
|
394
|
+
* @example
|
|
395
|
+
* widthToPrefix(390) // → "base"
|
|
396
|
+
* widthToPrefix(768) // → "md"
|
|
397
|
+
* widthToPrefix(1280) // → "xl"
|
|
398
|
+
* widthToPrefix(1536) // → "2xl"
|
|
399
|
+
*/
|
|
400
|
+
export function widthToPrefix(widthPx: number): TailwindBreakpointPrefix {
|
|
401
|
+
for (const { prefix, minPx } of BREAKPOINT_MIN_WIDTHS) {
|
|
402
|
+
if (widthPx >= minPx) return prefix;
|
|
403
|
+
}
|
|
404
|
+
return "base";
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// ---------------------------------------------------------------------------
|
|
408
|
+
// Override detection
|
|
409
|
+
// ---------------------------------------------------------------------------
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Return all breakpoint prefixes (other than `"base"`) at which the given
|
|
413
|
+
* property stem has an override in `className`. An override means a class with
|
|
414
|
+
* that stem exists at a prefix larger than `"base"`.
|
|
415
|
+
*
|
|
416
|
+
* Useful for rendering the "overridden at these breakpoints" indicators in the
|
|
417
|
+
* inspector.
|
|
418
|
+
*
|
|
419
|
+
* @example
|
|
420
|
+
* overriddenPrefixes("text-sm md:text-base lg:text-lg", "text")
|
|
421
|
+
* // → ["md", "lg"]
|
|
422
|
+
*/
|
|
423
|
+
export function overriddenPrefixes(
|
|
424
|
+
className: string,
|
|
425
|
+
stem: string,
|
|
426
|
+
): TailwindBreakpointPrefix[] {
|
|
427
|
+
const groups = parseClassGroups(className);
|
|
428
|
+
const result: TailwindBreakpointPrefix[] = [];
|
|
429
|
+
for (const prefix of ALL_PREFIXES) {
|
|
430
|
+
if (prefix === "base") continue;
|
|
431
|
+
const has = groups[prefix].some((tok) => {
|
|
432
|
+
const { utility } = parseClassToken(tok);
|
|
433
|
+
return utilityStem(utility) === stem;
|
|
434
|
+
});
|
|
435
|
+
if (has) result.push(prefix);
|
|
436
|
+
}
|
|
437
|
+
return result;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Reset a property at a breakpoint back to its base value by removing the
|
|
442
|
+
* prefixed override. Equivalent to `removePropertyClass(className, prefix, stem)`
|
|
443
|
+
* but named to match the "Reset to base" UI affordance.
|
|
444
|
+
*/
|
|
445
|
+
export function resetToBase(
|
|
446
|
+
className: string,
|
|
447
|
+
prefix: TailwindBreakpointPrefix,
|
|
448
|
+
stem: string,
|
|
449
|
+
): string {
|
|
450
|
+
if (prefix === "base") return className;
|
|
451
|
+
return removePropertyClass(className, prefix, stem);
|
|
452
|
+
}
|