create-shopify-scss-autofill 0.2.0 → 0.3.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.
@@ -79,18 +79,7 @@ function parseArgs(argv) {
79
79
  return out
80
80
  }
81
81
 
82
- function makeLowRangeCoefficients() {
83
- const out = {}
84
- for (let i = 0; i <= 50; i += 5) {
85
- const key = `scale-${String(i).padStart(2, '0')}`
86
- out[key] = Number((i / 100).toFixed(2))
87
- }
88
- return out
89
- }
90
-
91
82
  function makeDefaultConfig() {
92
- const lowRange = makeLowRangeCoefficients()
93
-
94
83
  return {
95
84
  $schema: './tools/scss-kit/schema.json',
96
85
  design: {
@@ -104,6 +93,7 @@ function makeDefaultConfig() {
104
93
  },
105
94
  autofill: {
106
95
  function: 'r.resp',
96
+ vwFunction: 'r.vw',
107
97
  mobileMax: 850,
108
98
  scanDirs: ['src/styles'],
109
99
  output: 'src/styles/_responsive-autofill.generated.scss',
@@ -126,30 +116,82 @@ function makeDefaultConfig() {
126
116
  },
127
117
  coefficients: {
128
118
  mobile: {
129
- ...lowRange,
130
- h1: 0.5,
131
- h2: 0.625,
132
- h3: 0.75,
133
- body: 0.857,
134
- small: 1,
135
- 'section-gap': 0.5,
136
- 'card-gap': 0.6,
137
- 'element-gap': 0.75,
138
- 'button-text': 1,
139
- icon: 0.67,
119
+ h1: 0.52,
120
+ h2: 0.55,
121
+ h3: 0.55,
122
+ h4: 0.6,
123
+ h5: 0.6,
124
+ h6: 0.6,
125
+ body: 0.6,
126
+ small: 0.65,
127
+ quote: 0.55,
128
+ 'button-text': 0.6,
129
+ 'nav-link': 0.6,
130
+ 'form-label': 0.6,
131
+ 'form-input': 0.6,
132
+ 'price-large': 0.5,
133
+ 'price-small': 0.6,
134
+ badge: 0.65,
135
+ breadcrumb: 0.65,
136
+ },
137
+ desktop: {
138
+ h1: 0.58,
139
+ h2: 0.6,
140
+ h3: 0.6,
141
+ h4: 0.6,
142
+ h5: 0.65,
143
+ h6: 0.65,
144
+ body: 0.65,
145
+ small: 0.65,
146
+ quote: 0.6,
147
+ 'button-text': 0.65,
148
+ 'nav-link': 0.65,
149
+ 'form-label': 0.65,
150
+ 'form-input': 0.65,
151
+ 'price-large': 0.55,
152
+ 'price-small': 0.65,
153
+ badge: 0.7,
154
+ breadcrumb: 0.7,
155
+ },
156
+ },
157
+ floors: {
158
+ mobile: {
159
+ h1: '24px',
160
+ h2: '20px',
161
+ h3: '18px',
162
+ h4: '16px',
163
+ h5: '14px',
164
+ h6: '14px',
165
+ body: '14px',
166
+ small: '12px',
167
+ quote: '14px',
168
+ 'button-text': '14px',
169
+ 'nav-link': '14px',
170
+ 'form-label': '14px',
171
+ 'form-input': '16px',
172
+ 'price-large': '22px',
173
+ 'price-small': '14px',
174
+ badge: '11px',
175
+ breadcrumb: '11px',
140
176
  },
141
177
  desktop: {
142
- ...lowRange,
143
- h1: 0.625,
144
- h2: 0.67,
145
- h3: 0.75,
146
- body: 0.875,
147
- small: 1,
148
- 'section-gap': 0.5,
149
- 'card-gap': 0.6,
150
- 'element-gap': 0.67,
151
- 'button-text': 1,
152
- icon: 0.625,
178
+ h1: '28px',
179
+ h2: '22px',
180
+ h3: '18px',
181
+ h4: '16px',
182
+ h5: '14px',
183
+ h6: '14px',
184
+ body: '14px',
185
+ small: '12px',
186
+ quote: '16px',
187
+ 'button-text': '14px',
188
+ 'nav-link': '14px',
189
+ 'form-label': '14px',
190
+ 'form-input': '14px',
191
+ 'price-large': '24px',
192
+ 'price-small': '14px',
193
+ badge: '12px',
194
+ breadcrumb: '12px',
153
195
  },
154
196
  },
155
197
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-shopify-scss-autofill",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Scaffold scss-kit (SCSS workflow + responsive autofill) for Shopify theme development.",
5
5
  "keywords": [
6
6
  "shopify",
@@ -1,6 +1,6 @@
1
1
  # scss-kit(本仓库内置工具)
2
2
 
3
- 目的:在 Shopify ThemeKit 开发里实现“写 SCSS(src/styles)→ 编译 CSS(assets)→ theme watch 上传”,并提供一套基于 `r.resp(pc, mobile, desktopType[, mobileType])` 的移动端覆盖自动生成能力。
3
+ 目的:在 Shopify ThemeKit 开发里实现“写 SCSS(src/styles)→ 编译 CSS(assets)→ theme watch 上传”,并提供一套基于 `r.resp(pc, mobile, desktopType[, mobileType])` 与 `r.vw(pc, mobile)` 的移动端覆盖自动生成能力。
4
4
 
5
5
  ## 拷贝接入(新项目)
6
6
 
@@ -57,6 +57,72 @@
57
57
  - 顶部 `@use "./_responsive-autofill.<page>.generated" as auto;`
58
58
  - 底部 `@include auto.responsive_autofill_overrides();`(确保覆盖顺序)
59
59
 
60
+ 推荐约定:
61
+
62
+ - 字体/需要下限兜底的值:使用 `r.resp(pc, mobile, desktopType[, mobileType])`
63
+ - 大多数间距(padding/margin/gap):使用 `r.vw(pc, mobile)`
64
+ - 少量间距若确实需要最小值:改用 `r.resp(...)`(不要给 `r.vw` 追加参数)
65
+
66
+ `r.vw(pc, mobile)` 的行为:
67
+
68
+ - PC 输出 `min(vw, px)`,例如 `r.vw(40px, 24px)` → `min(2.0833vw, 40px)`
69
+ - 自动生成的移动端覆盖直接输出 `vw`,例如 `3.2vw`
70
+
71
+ ## Responsive 函数目录(完整)
72
+
73
+ 以下函数都定义在 `src/styles/_responsive.scss`(由 `scss-kit` 生成):
74
+
75
+ ### 业务侧常用(推荐直接调用)
76
+
77
+ - `r.resp($pc, $mobile, $type, $mobile-type: null)`
78
+ - 用途:用于“字体/需要下限兜底”的场景。
79
+ - PC 输出:`clamp_pc($pc, min_px($pc, $type, desktop))`。
80
+ - Mobile 输出:由自动生成器扫描 `r.resp(...)` 后,在 `@media` 中生成 `clamp_mb(...)` 覆盖。
81
+ - 第 3/4 参数:第 3 个是 PC type;第 4 个是 mobile type(可省略,省略时沿用第 3 个)。
82
+ - 示例:`font-size: r.resp(40px, 24px, h1, h2);`
83
+
84
+ - `r.vw($pc, $mobile)`
85
+ - 用途:用于“间距/尺寸优先流式”的场景(如 padding/margin/gap/宽高)。
86
+ - PC 输出:`min(vw, px)`(防止超大屏继续放大)。
87
+ - Mobile 输出:由自动生成器扫描 `r.vw(...)` 后,在 `@media` 中生成纯 `vw` 覆盖。
88
+ - 说明:`r.vw` 仅保留两个参数;若你需要最小值下限,请使用 `r.resp(...)`。
89
+ - 示例:`margin-top: r.vw(40px, 24px);`
90
+
91
+ ### 由 `r.resp` / `r.vw` 间接使用(一般不直接写)
92
+
93
+ - `r.min_px($value, $type, $range: mobile, $override-coef: null, $override-floor: null)`
94
+ - 用途:计算 clamp 最小值。
95
+ - 规则:
96
+ - 命名 type:按 `max(designPx * coef(type), floor(type))` 计算(floor 来自 `floors.mobile/desktop`)。
97
+ - 数字 type:可直接传系数(例如 `0.6`),此时跳过 type 表查询;可选叠加 `override-floor`。
98
+
99
+ - `r.coef($type, $range: mobile)`
100
+ - 用途:读取 `coefficients.mobile/desktop` 中对应 type 的系数。
101
+
102
+ - `r._floor($type, $range: mobile)`
103
+ - 用途:读取 `floors.mobile/desktop` 中对应 type 的绝对兜底值。
104
+
105
+ - `r.clamp_pc($pc, $min)`
106
+ - 用途:生成 PC clamp。
107
+ - 结构:`clamp($min, calc($pc * var(--px-to-vw)), $pc)`。
108
+
109
+ - `r.clamp_mb($mobile, $min)`
110
+ - 用途:生成移动端 clamp。
111
+ - 结构:`clamp($min, calc($mobile * var(--px-to-vw-mb)), $mobile)`。
112
+
113
+ - `r.vw_pc($pc)`
114
+ - 用途:生成 PC 端 `vw` 并带上限。
115
+ - 结构:`min(calc($pc * var(--px-to-vw)), $pc)`。
116
+
117
+ - `r.vw_mb($mobile)`
118
+ - 用途:生成移动端纯 `vw`。
119
+ - 结构:`calc($mobile * var(--px-to-vw-mb))`。
120
+
121
+ ### 内部工具函数(不建议业务直接使用)
122
+
123
+ - `_to-px($value)`:把无单位数字转为 `px`。
124
+ - `_to-num($value)`:把 `px` 或无单位数字转为纯数字(其他单位会报错)。
125
+
60
126
  ## CSS 编译(safe mode)
61
127
 
62
128
  `npm run css:watch` 默认走 safe mode:
@@ -67,6 +67,7 @@ function getMobileMax(cfg) {
67
67
 
68
68
  function getAutofill(cfg) {
69
69
  const fn = cfg?.autofill?.function ?? 'r.resp'
70
+ const vwFn = cfg?.autofill?.vwFunction
70
71
  const mobileMax = getMobileMax(cfg)
71
72
  const scanDirs = cfg?.autofill?.scanDirs ?? [
72
73
  cfg?.paths?.scssSrcDir ?? 'src/styles',
@@ -82,9 +83,27 @@ function getAutofill(cfg) {
82
83
  }
83
84
  const ns = parts[0]
84
85
  const name = parts[1]
86
+
87
+ const defaultVwFn = `${ns}.vw`
88
+ const vwParts = String(vwFn ?? defaultVwFn).split('.')
89
+ if (vwParts.length !== 2 || !vwParts[0] || !vwParts[1]) {
90
+ throw new Error(
91
+ `Invalid autofill.vwFunction: ${String(
92
+ vwFn
93
+ )}. Expected format: <ns>.<name> (e.g. ${defaultVwFn})`
94
+ )
95
+ }
96
+ if (vwParts[0] !== ns) {
97
+ throw new Error(
98
+ `autofill.vwFunction namespace must match autofill.function namespace (${ns})`
99
+ )
100
+ }
101
+ const vwName = vwParts[1]
102
+
85
103
  return {
86
104
  ns,
87
105
  name,
106
+ vwName,
88
107
  mobileMax,
89
108
  scanDirs,
90
109
  outputAbs: path.join(ROOT, output),
@@ -139,8 +158,7 @@ function splitTopLevelArgs(argsStr) {
139
158
  return args
140
159
  }
141
160
 
142
- function replaceRespCalls(value, { ns, name }) {
143
- const token = `${ns}.${name}(`
161
+ function replaceFunctionCalls(value, token, mapArgsToReplacement) {
144
162
  let out = ''
145
163
  let idx = 0
146
164
  let changed = false
@@ -183,11 +201,9 @@ function replaceRespCalls(value, { ns, name }) {
183
201
 
184
202
  const argsStr = value.slice(argsStart, i)
185
203
  const args = splitTopLevelArgs(argsStr)
186
- if (args.length >= 3) {
187
- const mobile = args[1]
188
- const desktopType = args[2]
189
- const mobileType = args.length >= 4 ? args[3] : desktopType
190
- out += `${ns}.clamp_mb(${mobile}, ${ns}.min_px(${mobile}, ${mobileType}, mobile))`
204
+ const replacement = mapArgsToReplacement(args)
205
+ if (replacement != null) {
206
+ out += replacement
191
207
  changed = true
192
208
  } else {
193
209
  // Not enough args: keep original call.
@@ -200,6 +216,33 @@ function replaceRespCalls(value, { ns, name }) {
200
216
  return { value: out.trim(), changed }
201
217
  }
202
218
 
219
+ function replaceAutofillCalls(value, { ns, name, vwName }) {
220
+ const respToken = `${ns}.${name}(`
221
+ const replacedResp = replaceFunctionCalls(value, respToken, (args) => {
222
+ if (args.length < 3) return null
223
+ const mobile = args[1]
224
+ const desktopType = args[2]
225
+ const mobileType = args.length >= 4 ? args[3] : desktopType
226
+ return `${ns}.clamp_mb(${mobile}, ${ns}.min_px(${mobile}, ${mobileType}, mobile))`
227
+ })
228
+
229
+ const vwToken = `${ns}.${vwName}(`
230
+ const replacedVw = replaceFunctionCalls(
231
+ replacedResp.value,
232
+ vwToken,
233
+ (args) => {
234
+ if (args.length < 2) return null
235
+ const mobile = args[1]
236
+ return `${ns}.vw_mb(${mobile})`
237
+ }
238
+ )
239
+
240
+ return {
241
+ value: replacedVw.value.trim(),
242
+ changed: replacedResp.changed || replacedVw.changed,
243
+ }
244
+ }
245
+
203
246
  function combineSelectors(parents, children) {
204
247
  const out = []
205
248
  for (const p of parents) {
@@ -216,7 +259,7 @@ function combineSelectors(parents, children) {
216
259
  return out
217
260
  }
218
261
 
219
- function scanScssForAutofill_legacy(absFilePath, { ns, name }) {
262
+ function scanScssForAutofill_legacy(absFilePath, { ns, name, vwName }) {
220
263
  const raw = fs.readFileSync(absFilePath, 'utf8')
221
264
  const lines = raw.split(/\r?\n/)
222
265
 
@@ -275,7 +318,7 @@ function scanScssForAutofill_legacy(absFilePath, { ns, name }) {
275
318
  value = value.replace(/\s!important\s*$/i, '').trim()
276
319
  }
277
320
 
278
- const replaced = replaceRespCalls(value, { ns, name })
321
+ const replaced = replaceAutofillCalls(value, { ns, name, vwName })
279
322
  if (!replaced.changed) continue
280
323
 
281
324
  const finalValue = important
@@ -290,7 +333,7 @@ function scanScssForAutofill_legacy(absFilePath, { ns, name }) {
290
333
  return rules
291
334
  }
292
335
 
293
- function scanScssForAutofill_ast(absFilePath, { ns, name }) {
336
+ function scanScssForAutofill_ast(absFilePath, { ns, name, vwName }) {
294
337
  // Lazy-load optional deps so `init` can run before `npm install`.
295
338
  /** @type {typeof import('postcss')} */
296
339
  let postcss
@@ -332,7 +375,7 @@ function scanScssForAutofill_ast(absFilePath, { ns, name }) {
332
375
  const property = String(child.prop)
333
376
  let value = String(child.value ?? '').trim()
334
377
 
335
- const replaced = replaceRespCalls(value, { ns, name })
378
+ const replaced = replaceAutofillCalls(value, { ns, name, vwName })
336
379
  if (!replaced.changed) continue
337
380
 
338
381
  const finalValue = child.important
@@ -362,12 +405,12 @@ function scanScssForAutofill_ast(absFilePath, { ns, name }) {
362
405
  return rules
363
406
  }
364
407
 
365
- function scanScssForAutofill(absFilePath, { ns, name }) {
408
+ function scanScssForAutofill(absFilePath, { ns, name, vwName }) {
366
409
  try {
367
- return scanScssForAutofill_ast(absFilePath, { ns, name })
410
+ return scanScssForAutofill_ast(absFilePath, { ns, name, vwName })
368
411
  } catch (e) {
369
412
  // Fallback for edge cases where parser can't handle a file.
370
- return scanScssForAutofill_legacy(absFilePath, { ns, name })
413
+ return scanScssForAutofill_legacy(absFilePath, { ns, name, vwName })
371
414
  }
372
415
  }
373
416
 
@@ -388,7 +431,7 @@ function generateAutofillScss(cfg, collectedRules) {
388
431
  const header = `@use "./responsive" as ${ns};
389
432
 
390
433
  // Generated by scss-kit from ${CONFIG_NAME}
391
- // Source: scanned ${ns}.resp(pc, mobile, type) markers in scss.
434
+ // Source: scanned ${ns}.resp(pc, mobile, desktopType[, mobileType]) and ${ns}.vw(pc, mobile) markers in scss.
392
435
  // Do not edit this file directly; re-run: npm run scss-kit:responsive:generate
393
436
  \n`
394
437
 
@@ -529,10 +572,11 @@ function toScssMap(obj) {
529
572
  function generateResponsiveScss(cfg) {
530
573
  const mobileMap = toScssMap(cfg.coefficients.mobile)
531
574
  const desktopMap = toScssMap(cfg.coefficients.desktop)
575
+ const mobileFloorMap = toScssMap(cfg.floors?.mobile ?? {})
576
+ const desktopFloorMap = toScssMap(cfg.floors?.desktop ?? {})
532
577
 
533
578
  return `@use "sass:map";
534
579
  @use "sass:math";
535
- @use "sass:list";
536
580
  @use "sass:meta";
537
581
 
538
582
  // Generated by scss-kit from ${CONFIG_NAME}
@@ -546,6 +590,10 @@ $coef-mobile: ${mobileMap};
546
590
 
547
591
  $coef-desktop: ${desktopMap};
548
592
 
593
+ $floor-mobile: ${mobileFloorMap};
594
+
595
+ $floor-desktop: ${desktopFloorMap};
596
+
549
597
  @function _to-px($value) {
550
598
  @return if(math.is-unitless($value), $value * 1px, $value);
551
599
  }
@@ -571,25 +619,36 @@ $coef-desktop: ${desktopMap};
571
619
  @error "Unknown coef type: #{$type}";
572
620
  }
573
621
 
574
- // 根据 ${cfg.design.mobileWidth} 稿数值 + 系数,推导 clamp() 的最小值。
575
- @function min_px($mobile, $type, $range: mobile, $override-coef: null) {
576
- $v: _to-px($mobile);
622
+ @function _floor($type, $range: mobile) {
623
+ $table: if($range == desktop, $floor-desktop, $floor-mobile);
624
+ @if map.has-key($table, $type) {
625
+ @return map.get($table, $type);
626
+ }
627
+ @return null;
628
+ }
577
629
 
578
- // Mobile typography rule:
579
- // - Scale ${cfg.design.mobileWidth}px design to 375px proportion
580
- // - Apply readable floors: h1>=16px, h2>=14px, other text>=12px
581
- // - Ensure min <= max (design value)
582
- @if $range == mobile and $override-coef == null {
583
- $typography-types: (h1, h2, h3, body, small, button-text);
584
- @if list.index($typography-types, $type) {
585
- $scaled-375: $v * math.div(375, ${cfg.design.mobileWidth});
586
- $floor: if($type == h1, 16px, if($type == h2, 14px, 12px));
587
- @return math.min($v, math.max($scaled-375, $floor));
630
+ // 根据设计稿 px + 系数推导 clamp() 最小值:max(design * coef, floor)。
631
+ // $type 可传命名类型(字符串)或直接传数字系数(跳过系数表查询,不使用 floor)。
632
+ @function min_px($value, $type, $range: mobile, $override-coef: null, $override-floor: null) {
633
+ $v: _to-px($value);
634
+
635
+ // 直接传数字系数:跳过类型表,直接以系数计算
636
+ @if meta.type-of($type) == number {
637
+ $min: $v * $type;
638
+ @if $override-floor != null {
639
+ @return math.max($min, _to-px($override-floor));
588
640
  }
641
+ @return $min;
589
642
  }
590
643
 
591
644
  $c: if($override-coef == null, coef($type, $range), $override-coef);
592
- @return $v * $c;
645
+ $min: $v * $c;
646
+
647
+ $f: if($override-floor != null, _to-px($override-floor), _floor($type, $range));
648
+ @if $f {
649
+ @return math.max($min, $f);
650
+ }
651
+ @return $min;
593
652
  }
594
653
 
595
654
  // 生成桌面段 clamp:min + calc(pc * var(--px-to-vw)) + pc
@@ -606,6 +665,29 @@ $coef-desktop: ${desktopMap};
606
665
  @return clamp(#{$min}, calc(#{$n} * var(--px-to-vw-mb)), #{$v});
607
666
  }
608
667
 
668
+ // 直接输出 PC 段 vw,并用设计稿 px 做上限兜底。
669
+ @function vw_pc($pc) {
670
+ $v: _to-px($pc);
671
+ $n: _to-num($pc);
672
+ @return min(calc(#{$n} * var(--px-to-vw)), #{$v});
673
+ }
674
+
675
+ // 直接输出移动段 vw(无 clamp)。
676
+ @function vw_mb($mobile) {
677
+ $n: _to-num($mobile);
678
+ @return calc(#{$n} * var(--px-to-vw-mb));
679
+ }
680
+
681
+ // vw(): spacing-first helper(间距优先,默认不设最小值)
682
+ // - PC: min(vw, px)
683
+ // - Mobile: 纯 vw(通过 autofill 覆盖生成)
684
+ @function vw($pc, $mobile) {
685
+ @if meta.type-of($pc) != number or meta.type-of($mobile) != number {
686
+ @error "vw() expects numeric px values for both pc and mobile";
687
+ }
688
+ @return vw_pc($pc);
689
+ }
690
+
609
691
  // resp():
610
692
  // - arg3: desktop type
611
693
  // - arg4 (optional): mobile type, defaults to desktop type
@@ -25,6 +25,7 @@
25
25
  "type": "object",
26
26
  "properties": {
27
27
  "function": {"type": "string"},
28
+ "vwFunction": {"type": "string"},
28
29
  "mobileMax": {"type": "integer", "minimum": 1},
29
30
  "scanDirs": {"type": "array", "items": {"type": "string"}},
30
31
  "output": {"type": "string"},
@@ -47,6 +48,13 @@
47
48
  "mobile": {"type": "object", "additionalProperties": {"type": "number"}},
48
49
  "desktop": {"type": "object", "additionalProperties": {"type": "number"}}
49
50
  }
51
+ },
52
+ "floors": {
53
+ "type": "object",
54
+ "properties": {
55
+ "mobile": {"type": "object", "additionalProperties": {"type": "string"}},
56
+ "desktop": {"type": "object", "additionalProperties": {"type": "string"}}
57
+ }
50
58
  }
51
59
  }
52
60
  }