inkbridge 0.1.0-beta.21 → 0.1.0-beta.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -0
- package/code.js +15 -15
- package/manifest.json +1 -2
- package/package.json +40 -22
- package/scanner/border-dash-pattern-regression.ts +163 -0
- package/scanner/child-sizing-matrix-regression.ts +9 -0
- package/scanner/cli.ts +21 -5
- package/scanner/component-scanner.ts +1333 -77
- package/scanner/conditional-map-branch-regression.ts +180 -0
- package/scanner/css-token-reader.ts +66 -5
- package/scanner/dialog-content-gate-regression.ts +195 -0
- package/scanner/expression-evaluator-regression.ts +432 -0
- package/scanner/framework-adapter-shadcn-regression.ts +157 -1
- package/scanner/hidden-check-drift-regression.ts +125 -0
- package/scanner/horizontal-text-shrink-regression.ts +230 -0
- package/scanner/imported-array-map-regression.ts +195 -0
- package/scanner/inline-flex-regression.ts +5 -0
- package/scanner/intrinsic-sizing-regression.ts +333 -0
- package/scanner/portal-class-strip-regression.ts +109 -0
- package/scanner/responsive-hidden-inline-regression.ts +226 -0
- package/scanner/responsive-opt-in-regression.ts +212 -0
- package/scanner/select-root-flatten-regression.ts +314 -0
- package/scanner/space-between-single-child-regression.ts +163 -0
- package/scanner/story-args-resolution-regression.ts +311 -0
- package/scanner/story-dimensioning-regression.ts +76 -1
- package/scanner/style-map.ts +57 -0
- package/scanner/table-column-alignment-regression.ts +355 -0
- package/scanner/ternary-fragment-branch-regression.ts +196 -0
- package/scanner/text-truncate-regression.ts +481 -0
- package/scanner/types.ts +13 -0
- package/src/components/component-gen.ts +21 -38
- package/src/design-system/cva-master.ts +11 -18
- package/src/design-system/design-system.ts +36 -7
- package/src/design-system/frame-stabilizers.ts +109 -12
- package/src/design-system/preview-builder.ts +38 -0
- package/src/design-system/selectable-state.ts +8 -1
- package/src/design-system/story-builder.ts +62 -32
- package/src/design-system/story-dimensioning.ts +14 -3
- package/src/design-system/tag-predicates.ts +8 -0
- package/src/design-system/typography.ts +26 -0
- package/src/design-system/ui-builder.ts +188 -60
- package/src/effects/icon-builder.ts +8 -0
- package/src/framework-adapters/shadcn.ts +113 -0
- package/src/github/github.ts +22 -4
- package/src/layout/index.ts +4 -0
- package/src/layout/intrinsic-applier.ts +105 -0
- package/src/layout/intrinsic-sizing.ts +183 -0
- package/src/layout/layout-parser.ts +36 -0
- package/src/layout/parser/layout-mode.ts +14 -4
- package/src/layout/table-layout.ts +271 -0
- package/src/layout/text-truncate-pass.ts +151 -0
- package/src/layout/width-solver.ts +63 -17
- package/src/main.ts +37 -124
- package/src/plugin/config.ts +21 -0
- package/src/plugin/packs/pack-provider.ts +20 -4
- package/src/plugin/packs/packs.ts +14 -0
- package/src/render-engine-version.ts +1 -1
- package/src/tailwind/jsx-utils.ts +39 -0
- package/src/tailwind/node-ir.ts +8 -1
- package/src/tailwind/responsive-analyzer.ts +57 -3
- package/src/tailwind/tailwind.ts +344 -51
- package/src/text/index.ts +1 -0
- package/src/text/inline-text.ts +112 -12
- package/src/text/text-builder.ts +2 -2
- package/src/text/text-truncate.ts +101 -0
- package/src/tokens/tokens.ts +107 -16
- package/src/tokens/variables.ts +203 -46
- package/templates/scan-components-route.ts +8 -0
- package/ui.html +144 -43
package/src/tokens/variables.ts
CHANGED
|
@@ -1,4 +1,25 @@
|
|
|
1
|
-
import { TOKENS } from './tokens';
|
|
1
|
+
import { TOKENS, extractFontName, isConsumerOwnedToken } from './tokens';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Filter an entry map down to the consumer-owned keys for a given
|
|
5
|
+
* theme/group. Used by every demoFrame* to drop Tailwind defaults that
|
|
6
|
+
* leaked in through `@import "tailwindcss"` (e.g. `serif: Cambria`,
|
|
7
|
+
* `spacing.base: 0.25rem`, the full breakpoint scale). When no
|
|
8
|
+
* consumer-only scan is available (pre-scan, embedded fallback, older
|
|
9
|
+
* CLI output), `isConsumerOwnedToken` returns true for all keys —
|
|
10
|
+
* preserves the pre-Concern-#2 behaviour as a graceful fallback.
|
|
11
|
+
*/
|
|
12
|
+
function filterConsumerOwned(
|
|
13
|
+
theme: string,
|
|
14
|
+
group: string,
|
|
15
|
+
entries: Record<string, string>,
|
|
16
|
+
): Record<string, string> {
|
|
17
|
+
const out: Record<string, string> = {};
|
|
18
|
+
for (const key of Object.keys(entries)) {
|
|
19
|
+
if (isConsumerOwnedToken(theme, group, key)) out[key] = entries[key];
|
|
20
|
+
}
|
|
21
|
+
return out;
|
|
22
|
+
}
|
|
2
23
|
import { parseColor, colorToLabel, debug, normalizeThemeName, normalizeGroupName, normalizeSizeValue } from './colors';
|
|
3
24
|
|
|
4
25
|
// ---------------------------------------------------------------------------
|
|
@@ -902,6 +923,68 @@ export function createOrUpdateStylesFallback(): void {
|
|
|
902
923
|
/**
|
|
903
924
|
* Bind a frame's fill to a color variable (semantic token).
|
|
904
925
|
*/
|
|
926
|
+
/**
|
|
927
|
+
* Universal color application for any token slot — `fill` or `stroke`,
|
|
928
|
+
* for any node that has the corresponding property (frames, text). Tries
|
|
929
|
+
* variable binding first (so the result tracks theme mode changes), then
|
|
930
|
+
* falls back to a raw color value.
|
|
931
|
+
*
|
|
932
|
+
* Single source of truth that collapses the previously-duplicated bg-apply
|
|
933
|
+
* and border-apply blocks in tailwind.ts, cva-master.ts, and
|
|
934
|
+
* component-gen.ts. Adding a new color slot (e.g. focus-ring overlay,
|
|
935
|
+
* decorative SVG fill) becomes a one-liner against this helper instead of
|
|
936
|
+
* another copy-pasted variable-binding block.
|
|
937
|
+
*
|
|
938
|
+
* Returns `true` when EITHER the variable bound OR the literal color was
|
|
939
|
+
* applied — callers that need to schedule side effects (e.g. `strokeWeight
|
|
940
|
+
* = 1` only when a stroke was actually painted) gate on the return value.
|
|
941
|
+
*/
|
|
942
|
+
export function applyTokenColor(
|
|
943
|
+
node: SceneNode,
|
|
944
|
+
slot: 'fill' | 'stroke',
|
|
945
|
+
intent: { token?: string | null; value?: string | null; opacity?: number | null },
|
|
946
|
+
theme?: string,
|
|
947
|
+
): boolean {
|
|
948
|
+
// Figma's plugin API silently normalises a bound paint's `opacity` to 1
|
|
949
|
+
// on `node.fills = […]` reassignment when `boundVariables.color` is set.
|
|
950
|
+
// No combination of bake-before-bind, bake-after-bind, or Object.assign
|
|
951
|
+
// clones survives that normalisation (verified with traces of
|
|
952
|
+
// inputOpacity 0.6 → storedOpacity 1 on every variant). The only path
|
|
953
|
+
// that preserves partial alpha is to skip variable binding entirely and
|
|
954
|
+
// write a literal SOLID paint with `opacity` set. Trade-off: a theme /
|
|
955
|
+
// mode switch won't re-resolve the color for that fill. Acceptable
|
|
956
|
+
// because the consumer's `/N` modifier on the class was a compile-time
|
|
957
|
+
// decision in CSS too — Tailwind bakes the alpha at build time, not at
|
|
958
|
+
// theme-switch time.
|
|
959
|
+
const wantsPartialAlpha = intent.opacity != null && intent.opacity < 1;
|
|
960
|
+
if (intent.token && !wantsPartialAlpha) {
|
|
961
|
+
if (bindColorVariable(node, intent.token, slot, theme)) return true;
|
|
962
|
+
}
|
|
963
|
+
if (!intent.value) return false;
|
|
964
|
+
const color = parseColor(intent.value);
|
|
965
|
+
const opacity = intent.opacity != null
|
|
966
|
+
? Math.max(0, Math.min(1, intent.opacity))
|
|
967
|
+
: (color.a == null ? 1 : color.a);
|
|
968
|
+
const paint: SolidPaint = {
|
|
969
|
+
type: 'SOLID',
|
|
970
|
+
color: { r: color.r, g: color.g, b: color.b },
|
|
971
|
+
opacity,
|
|
972
|
+
};
|
|
973
|
+
try {
|
|
974
|
+
if (slot === 'fill' && 'fills' in node) {
|
|
975
|
+
node.fills = [paint];
|
|
976
|
+
return true;
|
|
977
|
+
}
|
|
978
|
+
if (slot === 'stroke' && 'strokes' in node) {
|
|
979
|
+
node.strokes = [paint];
|
|
980
|
+
return true;
|
|
981
|
+
}
|
|
982
|
+
} catch (_err) {
|
|
983
|
+
// Some node types reject the assignment; bail silently.
|
|
984
|
+
}
|
|
985
|
+
return false;
|
|
986
|
+
}
|
|
987
|
+
|
|
905
988
|
export function bindColorVariable(node: SceneNode, tokenKey: string, fillOrStroke: string, theme?: string): boolean {
|
|
906
989
|
const resolvedTheme =
|
|
907
990
|
(theme && (_themeColorVariables[theme] || _variableModeIds[theme])) ? theme :
|
|
@@ -914,20 +997,20 @@ export function bindColorVariable(node: SceneNode, tokenKey: string, fillOrStrok
|
|
|
914
997
|
if (!variable) return false;
|
|
915
998
|
|
|
916
999
|
try {
|
|
1000
|
+
// Bound paints render at opacity 1 always — Figma's plugin API
|
|
1001
|
+
// silently normalises a bound paint's `opacity` to 1 on `node.fills
|
|
1002
|
+
// = [paint]` reassignment regardless of what the input paint
|
|
1003
|
+
// carried. Callers that need partial alpha must skip binding and
|
|
1004
|
+
// use a literal SOLID paint instead (see `applyTokenColor`'s
|
|
1005
|
+
// `wantsPartialAlpha` short-circuit).
|
|
917
1006
|
if (fillOrStroke === 'fill' && 'fills' in node) {
|
|
918
|
-
const
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
}
|
|
922
|
-
fills[0] = figma.variables.setBoundVariableForPaint(fills[0] as SolidPaint, 'color', variable);
|
|
923
|
-
node.fills = fills;
|
|
1007
|
+
const seed: SolidPaint = { type: 'SOLID', color: { r: 1, g: 1, b: 1 } };
|
|
1008
|
+
const bound = figma.variables.setBoundVariableForPaint(seed, 'color', variable);
|
|
1009
|
+
node.fills = [bound];
|
|
924
1010
|
} else if (fillOrStroke === 'stroke' && 'strokes' in node) {
|
|
925
|
-
const
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
}
|
|
929
|
-
strokes[0] = figma.variables.setBoundVariableForPaint(strokes[0] as SolidPaint, 'color', variable);
|
|
930
|
-
node.strokes = strokes;
|
|
1011
|
+
const seed: SolidPaint = { type: 'SOLID', color: { r: 0.9, g: 0.9, b: 0.9 } };
|
|
1012
|
+
const bound = figma.variables.setBoundVariableForPaint(seed, 'color', variable);
|
|
1013
|
+
node.strokes = [bound];
|
|
931
1014
|
}
|
|
932
1015
|
return true;
|
|
933
1016
|
} catch (e) {
|
|
@@ -1135,11 +1218,58 @@ function makeSectionFrame(name: string): FrameNode {
|
|
|
1135
1218
|
return frame;
|
|
1136
1219
|
}
|
|
1137
1220
|
|
|
1221
|
+
// Column layout for the color demo grid:
|
|
1222
|
+
// * Up to MAX_COLOR_COLUMNS columns side-by-side.
|
|
1223
|
+
// * Minimum COLOR_MIN_ROWS_PER_COLUMN rows per column so small themes
|
|
1224
|
+
// don't render as a row of tiny single-entry columns.
|
|
1225
|
+
// * Once the column cap is hit, additional entries extend the per-column
|
|
1226
|
+
// height instead of opening a 4th column — keeps the grid visually
|
|
1227
|
+
// bounded on the Design System page.
|
|
1228
|
+
//
|
|
1229
|
+
// Effective rows-per-column = max(MIN, ceil(entries / MAX_COLUMNS)). So:
|
|
1230
|
+
// 12 entries → 1 column ×12 rows (under MIN, single column)
|
|
1231
|
+
// 30 entries → 2 columns ×15 rows (rounded up to MIN)
|
|
1232
|
+
// 45 entries → 3 columns ×15 rows
|
|
1233
|
+
// 60 entries → 3 columns ×20 rows (column cap holds, rows grow)
|
|
1234
|
+
const MAX_COLOR_COLUMNS = 3;
|
|
1235
|
+
const COLOR_MIN_ROWS_PER_COLUMN = 15;
|
|
1236
|
+
|
|
1138
1237
|
export function demoFrameColors(theme: string): FrameNode {
|
|
1139
1238
|
const frame = makeSectionFrame(theme.toUpperCase() + ' Colors');
|
|
1140
1239
|
frame.appendChild(makeSectionTitle(getThemeDisplayName(theme)));
|
|
1141
|
-
|
|
1142
|
-
|
|
1240
|
+
|
|
1241
|
+
// Inner horizontal grid: one VERTICAL column per slice of
|
|
1242
|
+
// rowsPerColumn entries. Outer section frame stays VERTICAL so
|
|
1243
|
+
// the title remains above the column row.
|
|
1244
|
+
const grid = figma.createFrame();
|
|
1245
|
+
grid.name = 'color-grid';
|
|
1246
|
+
grid.layoutMode = 'HORIZONTAL';
|
|
1247
|
+
grid.primaryAxisSizingMode = 'AUTO';
|
|
1248
|
+
grid.counterAxisSizingMode = 'AUTO';
|
|
1249
|
+
grid.itemSpacing = 32;
|
|
1250
|
+
grid.counterAxisAlignItems = 'MIN';
|
|
1251
|
+
grid.fills = [];
|
|
1252
|
+
grid.strokes = [];
|
|
1253
|
+
|
|
1254
|
+
const entries = Object.entries(filterConsumerOwned(theme, 'color', getThemeGroup(theme, 'color')));
|
|
1255
|
+
const rowsPerColumn = Math.max(
|
|
1256
|
+
COLOR_MIN_ROWS_PER_COLUMN,
|
|
1257
|
+
Math.ceil(entries.length / MAX_COLOR_COLUMNS),
|
|
1258
|
+
);
|
|
1259
|
+
let column: FrameNode | null = null;
|
|
1260
|
+
for (let i = 0; i < entries.length; i++) {
|
|
1261
|
+
if (i % rowsPerColumn === 0) {
|
|
1262
|
+
column = figma.createFrame();
|
|
1263
|
+
column.name = 'color-column-' + Math.floor(i / rowsPerColumn);
|
|
1264
|
+
column.layoutMode = 'VERTICAL';
|
|
1265
|
+
column.primaryAxisSizingMode = 'AUTO';
|
|
1266
|
+
column.counterAxisSizingMode = 'AUTO';
|
|
1267
|
+
column.itemSpacing = 12;
|
|
1268
|
+
column.fills = [];
|
|
1269
|
+
column.strokes = [];
|
|
1270
|
+
grid.appendChild(column);
|
|
1271
|
+
}
|
|
1272
|
+
const [key, val] = entries[i];
|
|
1143
1273
|
const row = figma.createFrame();
|
|
1144
1274
|
row.layoutMode = 'HORIZONTAL';
|
|
1145
1275
|
row.primaryAxisSizingMode = 'AUTO';
|
|
@@ -1157,12 +1287,19 @@ export function demoFrameColors(theme: string): FrameNode {
|
|
|
1157
1287
|
const label = makeBodyText(key + ' \u2014 ' + colorToLabel(val));
|
|
1158
1288
|
row.appendChild(swatch);
|
|
1159
1289
|
row.appendChild(label);
|
|
1160
|
-
|
|
1290
|
+
column!.appendChild(row);
|
|
1161
1291
|
}
|
|
1292
|
+
|
|
1293
|
+
frame.appendChild(grid);
|
|
1162
1294
|
return frame;
|
|
1163
1295
|
}
|
|
1164
1296
|
|
|
1165
|
-
export function demoFrameRadii(): FrameNode {
|
|
1297
|
+
export function demoFrameRadii(): FrameNode | null {
|
|
1298
|
+
const r: Record<string, string> = filterConsumerOwned(_defaultThemeName, 'radius', getThemeGroup(_defaultThemeName, 'radius'));
|
|
1299
|
+
// Nothing the consumer wrote → hide the section entirely so the
|
|
1300
|
+
// panel doesn't display an empty "Radii" placeholder.
|
|
1301
|
+
if (Object.keys(r).length === 0) return null;
|
|
1302
|
+
|
|
1166
1303
|
const frame = makeSectionFrame('Radii');
|
|
1167
1304
|
|
|
1168
1305
|
const row = figma.createFrame();
|
|
@@ -1172,8 +1309,6 @@ export function demoFrameRadii(): FrameNode {
|
|
|
1172
1309
|
row.itemSpacing = 16;
|
|
1173
1310
|
row.fills = [];
|
|
1174
1311
|
row.strokes = [];
|
|
1175
|
-
|
|
1176
|
-
const r: Record<string, string> = getThemeGroup(_defaultThemeName, 'radius');
|
|
1177
1312
|
for (const k of Object.keys(r)) {
|
|
1178
1313
|
const cell = figma.createFrame();
|
|
1179
1314
|
cell.layoutMode = 'VERTICAL';
|
|
@@ -1200,7 +1335,7 @@ export function demoFrameRadii(): FrameNode {
|
|
|
1200
1335
|
}
|
|
1201
1336
|
|
|
1202
1337
|
export function demoFrameFonts(theme: string): FrameNode | null {
|
|
1203
|
-
const fonts: Record<string, string> = getThemeGroup(theme, 'font');
|
|
1338
|
+
const fonts: Record<string, string> = filterConsumerOwned(theme, 'font', getThemeGroup(theme, 'font'));
|
|
1204
1339
|
const keys = Object.keys(fonts);
|
|
1205
1340
|
if (keys.length === 0) return null;
|
|
1206
1341
|
|
|
@@ -1208,22 +1343,13 @@ export function demoFrameFonts(theme: string): FrameNode | null {
|
|
|
1208
1343
|
frame.appendChild(makeSectionTitle(getThemeDisplayName(theme)));
|
|
1209
1344
|
|
|
1210
1345
|
for (const key of keys) {
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
return varMatch[1].split('-').map((w: string) => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
|
|
1219
|
-
}
|
|
1220
|
-
const lower = trimmed.toLowerCase().replace(/^["']|["']$/g, '');
|
|
1221
|
-
if (lower === 'ui-sans-serif' || lower === 'system-ui' || lower === 'sans-serif' || lower === 'serif' || lower === 'monospace' || lower === 'ui-monospace') continue;
|
|
1222
|
-
const cleaned = trimmed.replace(/^["']|["']$/g, '');
|
|
1223
|
-
if (cleaned) return cleaned;
|
|
1224
|
-
}
|
|
1225
|
-
return 'Inter';
|
|
1226
|
-
})();
|
|
1346
|
+
// Use the shared extractor so this preview agrees with the actual
|
|
1347
|
+
// rendering pipeline (`getThemeFontFamily` + render-time
|
|
1348
|
+
// `setRangeFontName`). The previous inline extractor only filtered
|
|
1349
|
+
// 6 keywords, missing `ui-serif`, `SFMono-Regular`, `Apple Color
|
|
1350
|
+
// Emoji`, `Menlo`, etc. — so the preview rendered "SFMono-Regular"
|
|
1351
|
+
// labels that Figma can't load (falling back to Inter visually).
|
|
1352
|
+
const family = extractFontName(fonts[key]) || 'Inter';
|
|
1227
1353
|
|
|
1228
1354
|
const cell = figma.createFrame();
|
|
1229
1355
|
cell.layoutMode = 'VERTICAL';
|
|
@@ -1242,7 +1368,7 @@ export function demoFrameFonts(theme: string): FrameNode | null {
|
|
|
1242
1368
|
}
|
|
1243
1369
|
|
|
1244
1370
|
export function demoFrameSpacing(): FrameNode | null {
|
|
1245
|
-
const spacing: Record<string, string> = getThemeGroup(_defaultThemeName, 'spacing');
|
|
1371
|
+
const spacing: Record<string, string> = filterConsumerOwned(_defaultThemeName, 'spacing', getThemeGroup(_defaultThemeName, 'spacing'));
|
|
1246
1372
|
const keys = Object.keys(spacing);
|
|
1247
1373
|
if (keys.length === 0) return null;
|
|
1248
1374
|
|
|
@@ -1273,21 +1399,40 @@ export function demoFrameSpacing(): FrameNode | null {
|
|
|
1273
1399
|
}
|
|
1274
1400
|
|
|
1275
1401
|
export function demoFrameFontSizes(): FrameNode | null {
|
|
1276
|
-
const sizes: Record<string, string> = getThemeGroup(_defaultThemeName, 'fontSize');
|
|
1402
|
+
const sizes: Record<string, string> = filterConsumerOwned(_defaultThemeName, 'fontSize', getThemeGroup(_defaultThemeName, 'fontSize'));
|
|
1277
1403
|
const keys = Object.keys(sizes);
|
|
1278
1404
|
if (keys.length === 0) return null;
|
|
1279
1405
|
|
|
1280
1406
|
const frame = makeSectionFrame('Font sizes');
|
|
1281
1407
|
|
|
1408
|
+
// Inner wrapping row so large tokens (3xl, 9xl, …) lay out horizontally
|
|
1409
|
+
// and roll to a new line when they overflow, instead of stacking into a
|
|
1410
|
+
// tall single column. Label text is just the token key (`9xl`) rendered
|
|
1411
|
+
// at its actual size — the visual IS the size, no need for a "—128px
|
|
1412
|
+
// sample" annotation.
|
|
1413
|
+
const row = figma.createFrame();
|
|
1414
|
+
row.name = 'fontsize-row';
|
|
1415
|
+
row.layoutMode = 'HORIZONTAL';
|
|
1416
|
+
row.layoutWrap = 'WRAP';
|
|
1417
|
+
row.primaryAxisSizingMode = 'FIXED';
|
|
1418
|
+
row.counterAxisSizingMode = 'AUTO';
|
|
1419
|
+
row.resize(720, row.height);
|
|
1420
|
+
row.itemSpacing = 32;
|
|
1421
|
+
row.counterAxisSpacing = 16;
|
|
1422
|
+
row.counterAxisAlignItems = 'MIN';
|
|
1423
|
+
row.fills = [];
|
|
1424
|
+
row.strokes = [];
|
|
1425
|
+
|
|
1282
1426
|
for (const k of keys) {
|
|
1283
1427
|
const px = pxFromSizeToken(sizes[k]);
|
|
1284
|
-
|
|
1428
|
+
row.appendChild(makeBodyText(k, { size: px }));
|
|
1285
1429
|
}
|
|
1430
|
+
frame.appendChild(row);
|
|
1286
1431
|
return frame;
|
|
1287
1432
|
}
|
|
1288
1433
|
|
|
1289
1434
|
export function demoFrameBreakpoints(): FrameNode | null {
|
|
1290
|
-
const breakpoints: Record<string, string> = getThemeGroup(_defaultThemeName, 'breakpoint');
|
|
1435
|
+
const breakpoints: Record<string, string> = filterConsumerOwned(_defaultThemeName, 'breakpoint', getThemeGroup(_defaultThemeName, 'breakpoint'));
|
|
1291
1436
|
const keys = Object.keys(breakpoints);
|
|
1292
1437
|
if (keys.length === 0) return null;
|
|
1293
1438
|
|
|
@@ -1334,19 +1479,30 @@ export function demoFrameBreakpoints(): FrameNode | null {
|
|
|
1334
1479
|
const SHADOW_SIZE_ORDER = ['2xs', 'xs', 'sm', 'md', 'lg', 'xl', '2xl'];
|
|
1335
1480
|
|
|
1336
1481
|
export function demoFrameShadows(theme: string): FrameNode | null {
|
|
1337
|
-
const shadows: Record<string, string> = getThemeGroup(theme, 'shadow');
|
|
1482
|
+
const shadows: Record<string, string> = filterConsumerOwned(theme, 'shadow', getThemeGroup(theme, 'shadow'));
|
|
1338
1483
|
const ordered = SHADOW_SIZE_ORDER.filter((k) => shadows[k]);
|
|
1339
1484
|
if (ordered.length === 0) return null;
|
|
1340
1485
|
|
|
1341
1486
|
const frame = makeSectionFrame(theme.toUpperCase() + ' Shadows');
|
|
1342
|
-
// Larger gap so big shadows (shadow-xl, shadow-2xl) don't visually overlap
|
|
1343
|
-
// the next swatch — the default itemSpacing is too tight for shadow-2xl
|
|
1344
|
-
// which extends ~38px below its rect.
|
|
1345
|
-
frame.itemSpacing = 56;
|
|
1346
1487
|
// Keep shadows from being clipped by the wrapper's bounds.
|
|
1347
1488
|
frame.clipsContent = false;
|
|
1348
1489
|
frame.appendChild(makeSectionTitle(getThemeDisplayName(theme)));
|
|
1349
1490
|
|
|
1491
|
+
// Inner horizontal row so swatches read left-to-right under the title.
|
|
1492
|
+
// The outer section frame stays VERTICAL so the title sits above. Larger
|
|
1493
|
+
// itemSpacing here so big shadows (shadow-xl, shadow-2xl) don't visually
|
|
1494
|
+
// overlap their neighbour — defaults are too tight for shadow-2xl which
|
|
1495
|
+
// extends ~38px past its rect.
|
|
1496
|
+
const row = figma.createFrame();
|
|
1497
|
+
row.name = 'shadow-row';
|
|
1498
|
+
row.layoutMode = 'HORIZONTAL';
|
|
1499
|
+
row.primaryAxisSizingMode = 'AUTO';
|
|
1500
|
+
row.counterAxisSizingMode = 'AUTO';
|
|
1501
|
+
row.itemSpacing = 56;
|
|
1502
|
+
row.fills = [];
|
|
1503
|
+
row.strokes = [];
|
|
1504
|
+
row.clipsContent = false;
|
|
1505
|
+
|
|
1350
1506
|
for (const k of ordered) {
|
|
1351
1507
|
const cell = figma.createFrame();
|
|
1352
1508
|
cell.layoutMode = 'VERTICAL';
|
|
@@ -1381,7 +1537,8 @@ export function demoFrameShadows(theme: string): FrameNode | null {
|
|
|
1381
1537
|
|
|
1382
1538
|
cell.appendChild(swatch);
|
|
1383
1539
|
cell.appendChild(makeBodyText(k, { size: 12 }));
|
|
1384
|
-
|
|
1540
|
+
row.appendChild(cell);
|
|
1385
1541
|
}
|
|
1542
|
+
frame.appendChild(row);
|
|
1386
1543
|
return frame;
|
|
1387
1544
|
}
|
|
@@ -59,6 +59,14 @@ export async function GET(request: Request): Promise<NextResponse> {
|
|
|
59
59
|
if (dtcgTokenPath) args.push('--dtcg-token-path', dtcgTokenPath);
|
|
60
60
|
|
|
61
61
|
const result = spawnSync(TSX, args, { cwd: CWD, encoding: 'utf-8' });
|
|
62
|
+
// Forward captured subprocess streams to the dev server's terminal.
|
|
63
|
+
// `spawnSync` with `encoding` captures stdout/stderr into the result
|
|
64
|
+
// object instead of inheriting them, so any `console.log` /
|
|
65
|
+
// `console.error` inside the scanner would otherwise be invisible.
|
|
66
|
+
// Reprinting here means scanner diagnostics surface in the same place
|
|
67
|
+
// as Next.js logs — no per-debug-session instrumentation needed.
|
|
68
|
+
if (result.stdout) process.stdout.write(result.stdout);
|
|
69
|
+
if (result.stderr) process.stderr.write(result.stderr);
|
|
62
70
|
if (result.status !== 0) {
|
|
63
71
|
throw new Error(result.stderr || 'Scanner exited with non-zero status');
|
|
64
72
|
}
|
package/ui.html
CHANGED
|
@@ -91,6 +91,48 @@
|
|
|
91
91
|
letter-spacing: 0.5px;
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
+
/* Advanced disclosure — `<details>`-based collapsible section.
|
|
95
|
+
Keeps DTCG-mode + sync knobs out of sight for CSS-first users
|
|
96
|
+
(the majority) but one click away when needed. */
|
|
97
|
+
.advanced-details {
|
|
98
|
+
margin: 0;
|
|
99
|
+
}
|
|
100
|
+
.advanced-details > summary {
|
|
101
|
+
cursor: pointer;
|
|
102
|
+
list-style: none;
|
|
103
|
+
font-size: 11px;
|
|
104
|
+
font-weight: 600;
|
|
105
|
+
color: #333;
|
|
106
|
+
text-transform: uppercase;
|
|
107
|
+
letter-spacing: 0.5px;
|
|
108
|
+
padding: 0;
|
|
109
|
+
margin-bottom: 12px;
|
|
110
|
+
user-select: none;
|
|
111
|
+
display: flex;
|
|
112
|
+
align-items: center;
|
|
113
|
+
gap: 6px;
|
|
114
|
+
}
|
|
115
|
+
.advanced-details > summary::-webkit-details-marker { display: none; }
|
|
116
|
+
.advanced-details > summary::before {
|
|
117
|
+
content: '▸';
|
|
118
|
+
font-size: 10px;
|
|
119
|
+
color: #666;
|
|
120
|
+
transition: transform 0.15s ease;
|
|
121
|
+
display: inline-block;
|
|
122
|
+
width: 8px;
|
|
123
|
+
text-align: center;
|
|
124
|
+
}
|
|
125
|
+
.advanced-details[open] > summary::before {
|
|
126
|
+
transform: rotate(90deg);
|
|
127
|
+
}
|
|
128
|
+
.advanced-details > summary > .summary-hint {
|
|
129
|
+
font-weight: 400;
|
|
130
|
+
font-size: 10px;
|
|
131
|
+
color: #888;
|
|
132
|
+
text-transform: none;
|
|
133
|
+
letter-spacing: 0;
|
|
134
|
+
}
|
|
135
|
+
|
|
94
136
|
.repo-display {
|
|
95
137
|
font-size: 11px;
|
|
96
138
|
color: #666;
|
|
@@ -439,48 +481,54 @@
|
|
|
439
481
|
<input type="text" id="settingsBranch" placeholder="main" value="main">
|
|
440
482
|
</div>
|
|
441
483
|
|
|
442
|
-
<div class="
|
|
443
|
-
<label>Token Source Mode</label>
|
|
444
|
-
<select id="settingsTokenSourceMode">
|
|
445
|
-
<option value="css">css (auto-discover CSS, fallback to DTCG)</option>
|
|
446
|
-
<option value="dtcg">dtcg (force DTCG file only)</option>
|
|
447
|
-
</select>
|
|
448
|
-
<p class="hint">CSS mode auto-discovers your globals.css and falls back to DTCG if not found. Use DTCG to force the legacy token file.</p>
|
|
449
|
-
</div>
|
|
484
|
+
<div class="divider"></div>
|
|
450
485
|
|
|
451
|
-
<
|
|
452
|
-
<
|
|
453
|
-
<input type="text" id="settingsTokenPath" placeholder="design-tokens/tokens.dtcg.json" value="design-tokens/tokens.dtcg.json">
|
|
454
|
-
<p class="hint">Path to the DTCG token file (used in dtcg mode or as fallback in auto mode)</p>
|
|
455
|
-
</div>
|
|
486
|
+
<details class="advanced-details" id="advancedTokenDetails">
|
|
487
|
+
<summary>Advanced — Token Sync <span class="summary-hint">(DTCG, push policy)</span></summary>
|
|
456
488
|
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
489
|
+
<div class="field">
|
|
490
|
+
<label>Token Source Mode</label>
|
|
491
|
+
<select id="settingsTokenSourceMode">
|
|
492
|
+
<option value="css">css (recommended — auto-discover globals.css)</option>
|
|
493
|
+
<option value="dtcg">dtcg (force DTCG file — Tokens Studio / Style Dictionary)</option>
|
|
494
|
+
</select>
|
|
495
|
+
<p class="hint">Most projects use CSS-first tokens (custom properties in <code>globals.css</code> or imported tokens.css). Pick <code>dtcg</code> only if your source of truth is a <code>tokens.dtcg.json</code> file.</p>
|
|
496
|
+
</div>
|
|
462
497
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
<input type="
|
|
466
|
-
<
|
|
498
|
+
<div class="field" id="cssTokenPathField">
|
|
499
|
+
<label>CSS Token Path (optional)</label>
|
|
500
|
+
<input type="text" id="settingsCssTokenPath" placeholder="src/app/tokens.css">
|
|
501
|
+
<p class="hint">Overrides CSS auto-discovery when set. Leave blank unless your tokens live outside <code>globals.css</code> and aren't imported from it.</p>
|
|
467
502
|
</div>
|
|
468
|
-
<p class="hint">When enabled, the plugin also commits <code>tokens.dtcg.json</code> as a generated artifact.</p>
|
|
469
|
-
</div>
|
|
470
503
|
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
<input type="
|
|
474
|
-
<
|
|
504
|
+
<div class="field" id="tokenPathField">
|
|
505
|
+
<label>DTCG Token Path</label>
|
|
506
|
+
<input type="text" id="settingsTokenPath" placeholder="design-tokens/tokens.dtcg.json">
|
|
507
|
+
<p class="hint">Path to the DTCG token file (used in dtcg mode or as fallback in css mode when no CSS file is found). Leave blank for the default <code>design-tokens/tokens.dtcg.json</code>.</p>
|
|
475
508
|
</div>
|
|
476
|
-
<p class="hint">Disabled by default. When enabled, new token keys can be added to code on push.</p>
|
|
477
|
-
</div>
|
|
478
509
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
510
|
+
<div class="field" id="syncDtcgOnPushField">
|
|
511
|
+
<div style="display:flex;align-items:center;gap:8px;">
|
|
512
|
+
<input type="checkbox" id="settingsSyncDtcgOnPush" style="width:auto;flex-shrink:0;">
|
|
513
|
+
<label for="settingsSyncDtcgOnPush" style="display:inline;margin:0;cursor:pointer;">Also update DTCG on Push to Code</label>
|
|
514
|
+
</div>
|
|
515
|
+
<p class="hint">When enabled, the plugin also commits <code>tokens.dtcg.json</code> as a generated artifact alongside the CSS file.</p>
|
|
516
|
+
</div>
|
|
517
|
+
|
|
518
|
+
<div class="field">
|
|
519
|
+
<div style="display:flex;align-items:center;gap:8px;">
|
|
520
|
+
<input type="checkbox" id="settingsAllowNewTokensFromFigma" style="width:auto;flex-shrink:0;">
|
|
521
|
+
<label for="settingsAllowNewTokensFromFigma" style="display:inline;margin:0;cursor:pointer;">Allow New Tokens from Figma</label>
|
|
522
|
+
</div>
|
|
523
|
+
<p class="hint">Disabled by default. When enabled, new token keys can be added to code on push.</p>
|
|
524
|
+
</div>
|
|
525
|
+
|
|
526
|
+
<div class="field" id="newTokenPrefixesField" style="display:none;">
|
|
527
|
+
<label>New Token Prefixes (optional)</label>
|
|
528
|
+
<input type="text" id="settingsNewTokenPrefixes" placeholder="chart-, brand-">
|
|
529
|
+
<p class="hint">Comma-separated. Only tokens with these prefixes can be added. Leave empty to allow all new tokens.</p>
|
|
530
|
+
</div>
|
|
531
|
+
</details>
|
|
484
532
|
|
|
485
533
|
<div class="divider"></div>
|
|
486
534
|
|
|
@@ -497,6 +545,16 @@
|
|
|
497
545
|
|
|
498
546
|
<div class="divider"></div>
|
|
499
547
|
|
|
548
|
+
<div class="section-title">Dev Server (Optional)</div>
|
|
549
|
+
|
|
550
|
+
<div class="field">
|
|
551
|
+
<label>Dev Server Port</label>
|
|
552
|
+
<input type="text" id="settingsDevServerPort" placeholder="e.g., 3000" inputmode="numeric" maxlength="5">
|
|
553
|
+
<p class="hint">Pin the scanner to one localhost port. Leave blank to auto-discover across 3000 / 4000 / 5173. Useful when you're running multiple local projects on different ports.</p>
|
|
554
|
+
</div>
|
|
555
|
+
|
|
556
|
+
<div class="divider"></div>
|
|
557
|
+
|
|
500
558
|
<div class="section-title">Display (Optional)</div>
|
|
501
559
|
|
|
502
560
|
<div class="field">
|
|
@@ -769,8 +827,14 @@
|
|
|
769
827
|
var newTokenPrefixesField = document.getElementById('newTokenPrefixesField');
|
|
770
828
|
var settingsToken = document.getElementById('settingsToken');
|
|
771
829
|
var settingsProjectName = document.getElementById('settingsProjectName');
|
|
830
|
+
var settingsDevServerPort = document.getElementById('settingsDevServerPort');
|
|
772
831
|
var settingsLicenseKey = document.getElementById('settingsLicenseKey');
|
|
773
832
|
|
|
833
|
+
// Cached configured dev-server port — empty string means "auto-discover".
|
|
834
|
+
// Updated on `config-loaded` and at save time so the port override takes
|
|
835
|
+
// effect for subsequent fetches without requiring a plugin reload.
|
|
836
|
+
var configuredDevServerPort = '';
|
|
837
|
+
|
|
774
838
|
// Current detected changes
|
|
775
839
|
var detectedChanges = { tokens: true, components: [] };
|
|
776
840
|
var lastTokenSourceInfo = null;
|
|
@@ -1092,17 +1156,35 @@
|
|
|
1092
1156
|
|
|
1093
1157
|
// Dev server fetch - runs in the UI iframe which has network access
|
|
1094
1158
|
// (code.js sandbox cannot make fetch calls)
|
|
1095
|
-
var
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1159
|
+
var DEFAULT_DEV_SERVER_PORTS = [3000, 4000, 5173];
|
|
1160
|
+
|
|
1161
|
+
// Resolve the dev-server ports to try. A configured `devServerPort`
|
|
1162
|
+
// (validated to 1-65535 by the config normaliser) collapses the
|
|
1163
|
+
// sweep to that single port — useful when the user has multiple
|
|
1164
|
+
// local projects on different ports.
|
|
1165
|
+
function getDevServerPorts() {
|
|
1166
|
+
var raw = (configuredDevServerPort || '').trim();
|
|
1167
|
+
if (!raw) return DEFAULT_DEV_SERVER_PORTS;
|
|
1168
|
+
var n = parseInt(raw, 10);
|
|
1169
|
+
if (!isFinite(n) || n < 1 || n > 65535) return DEFAULT_DEV_SERVER_PORTS;
|
|
1170
|
+
return [n];
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
function getScanComponentsEndpoints() {
|
|
1174
|
+
var ports = getDevServerPorts();
|
|
1175
|
+
var out = [];
|
|
1176
|
+
for (var i = 0; i < ports.length; i++) {
|
|
1177
|
+
out.push('http://localhost:' + ports[i] + '/api/inkbridge/scan-components');
|
|
1178
|
+
}
|
|
1179
|
+
return out;
|
|
1180
|
+
}
|
|
1100
1181
|
|
|
1101
1182
|
var DEV_SERVER_TIMEOUT_MS = 15000;
|
|
1102
1183
|
|
|
1103
1184
|
async function fetchComponentDefs() {
|
|
1104
|
-
|
|
1105
|
-
|
|
1185
|
+
var endpoints = getScanComponentsEndpoints();
|
|
1186
|
+
for (var i = 0; i < endpoints.length; i++) {
|
|
1187
|
+
var url = endpoints[i];
|
|
1106
1188
|
try {
|
|
1107
1189
|
var controller = new AbortController();
|
|
1108
1190
|
var timeoutId = setTimeout(function() { controller.abort(); }, DEV_SERVER_TIMEOUT_MS);
|
|
@@ -1255,7 +1337,7 @@
|
|
|
1255
1337
|
// UI-side license validation (code.js sandbox has no network)
|
|
1256
1338
|
if (msg.type === 'validate-license') {
|
|
1257
1339
|
var licenseResult = { tier: 'free', valid: false };
|
|
1258
|
-
var validatePorts =
|
|
1340
|
+
var validatePorts = getDevServerPorts();
|
|
1259
1341
|
for (var vp = 0; vp < validatePorts.length; vp++) {
|
|
1260
1342
|
try {
|
|
1261
1343
|
var vUrl = 'http://localhost:' + validatePorts[vp] + '/api/plugin/validate?key=' + encodeURIComponent(msg.key);
|
|
@@ -1426,10 +1508,25 @@
|
|
|
1426
1508
|
settingsNewTokenPrefixes.value = Array.isArray(config.newTokenPrefixes) ? config.newTokenPrefixes.join(', ') : (typeof config.newTokenPrefixes === 'string' ? config.newTokenPrefixes : '');
|
|
1427
1509
|
newTokenPrefixesField.style.display = config.allowNewTokensFromFigma === true ? 'block' : 'none';
|
|
1428
1510
|
settingsProjectName.value = config.projectName || '';
|
|
1511
|
+
settingsDevServerPort.value = config.devServerPort || '';
|
|
1512
|
+
configuredDevServerPort = (config.devServerPort || '').trim();
|
|
1429
1513
|
configuredTokenSourceMode = tokenSourceMode;
|
|
1430
1514
|
applyTokenSourceModeVisibility(tokenSourceMode);
|
|
1431
1515
|
setTokenSourceInfo(lastTokenSourceInfo);
|
|
1432
1516
|
|
|
1517
|
+
// Auto-expand the Advanced disclosure when the consumer has any
|
|
1518
|
+
// non-default token-sync setting. Default state (css mode, no
|
|
1519
|
+
// overrides, no DTCG sync, default new-token policy) stays
|
|
1520
|
+
// collapsed — clean form for the CSS-first majority.
|
|
1521
|
+
var advancedHasNonDefault =
|
|
1522
|
+
tokenSourceMode !== 'css'
|
|
1523
|
+
|| (config.tokenPath && config.tokenPath !== 'design-tokens/tokens.dtcg.json')
|
|
1524
|
+
|| (config.cssTokenPath && config.cssTokenPath.length > 0)
|
|
1525
|
+
|| config.syncDtcgOnPush === true
|
|
1526
|
+
|| config.allowNewTokensFromFigma === true;
|
|
1527
|
+
var advancedTokenDetails = document.getElementById('advancedTokenDetails');
|
|
1528
|
+
if (advancedTokenDetails) advancedTokenDetails.open = advancedHasNonDefault;
|
|
1529
|
+
|
|
1433
1530
|
// Update push view repo info
|
|
1434
1531
|
if (config.owner && config.repo) {
|
|
1435
1532
|
repoLabel.textContent = config.owner + '/' + config.repo;
|
|
@@ -1650,10 +1747,14 @@
|
|
|
1650
1747
|
.map(function(v) { return v.trim(); })
|
|
1651
1748
|
.filter(function(v) { return v.length > 0; }),
|
|
1652
1749
|
projectName: settingsProjectName.value.trim(),
|
|
1750
|
+
devServerPort: settingsDevServerPort.value.trim(),
|
|
1653
1751
|
token: settingsToken.value.trim() || null,
|
|
1654
1752
|
licenseKey: settingsLicenseKey.value.trim() || null
|
|
1655
1753
|
}
|
|
1656
1754
|
}, '*');
|
|
1755
|
+
// Keep the in-page port cache in sync so subsequent dev-server
|
|
1756
|
+
// fetches respect the new value without waiting for a reload.
|
|
1757
|
+
configuredDevServerPort = settingsDevServerPort.value.trim();
|
|
1657
1758
|
};
|
|
1658
1759
|
|
|
1659
1760
|
settingsTokenSourceMode.onchange = function() {
|