@numbered/docs-to-context 0.3.1 → 0.4.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.
@@ -0,0 +1,184 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Convert pixel values to Tailwind CSS grid column classes.
4
+
5
+ Compatible with @numbered/tailwind-fluid-layout-system.
6
+
7
+ Usage:
8
+ python pixels_to_columns.py <pixels> [--columns N] [--mockup N] [--gutter N] [--margin N]
9
+
10
+ Examples:
11
+ # Desktop: 330px element in 24-column grid
12
+ python pixels_to_columns.py 330 --columns 24 --mockup 1440 --gutter 24 --margin 24
13
+
14
+ # Mobile: 150px element in 6-column grid
15
+ python pixels_to_columns.py 150 --columns 6 --mockup 375 --gutter 12 --margin 12
16
+ """
17
+
18
+ import argparse
19
+ import json
20
+ import sys
21
+
22
+
23
+ def pixels_to_columns(
24
+ pixels: float,
25
+ grid_columns: int = 24,
26
+ mockup_width: int = 1440,
27
+ gutter: int = 24,
28
+ margin: int = 24
29
+ ) -> dict:
30
+ """
31
+ Convert pixel value to Tailwind grid column span class.
32
+
33
+ Args:
34
+ pixels: The pixel value to convert
35
+ grid_columns: Number of grid columns (6 mobile, 24 desktop)
36
+ mockup_width: Total mockup width in pixels (375 mobile, 1440 desktop)
37
+ gutter: Gutter size between columns in pixels
38
+ margin: Outer margins in pixels
39
+
40
+ Returns:
41
+ Dictionary with className, columns, actualWidth, pixelDifference, and gridConfig
42
+ """
43
+ # Calculate available width for content (mockup width minus margins)
44
+ content_width = mockup_width - (2 * margin)
45
+
46
+ # Calculate column width: N columns have (N-1) gutters between them
47
+ # content_width = (grid_columns * column_width) + ((grid_columns - 1) * gutter)
48
+ # Solving for column_width:
49
+ total_gutter_space = (grid_columns - 1) * gutter
50
+ total_column_space = content_width - total_gutter_space
51
+ column_width = total_column_space / grid_columns
52
+
53
+ # Build grid config once (DRY)
54
+ grid_config = {
55
+ "columns": grid_columns,
56
+ "mockupWidth": mockup_width,
57
+ "gutter": gutter,
58
+ "margin": margin,
59
+ "columnWidth": round(column_width),
60
+ "contentWidth": round(content_width)
61
+ }
62
+
63
+ def span_width(n, suffix=""):
64
+ """Calculate width for span-w-N with optional suffix."""
65
+ # Base: N columns + (N-1) gutters
66
+ base = (n * column_width) + ((n - 1) * gutter)
67
+ if suffix == "-wide":
68
+ return base + gutter
69
+ elif suffix == "-wider":
70
+ return base + (gutter * 2)
71
+ return base
72
+
73
+ # Check if value is less than 1 column - use gutter-based matching
74
+ if pixels < column_width:
75
+ # Gutter multiples to check: 0.5, 1, 1.5, 2, 2.5, 3, 4, 5, 6
76
+ gutter_multiples = [0.5, 1, 1.5, 2, 2.5, 3, 4, 5, 6]
77
+ best_gutter = None
78
+ best_gutter_diff = float('inf')
79
+
80
+ for mult in gutter_multiples:
81
+ gutter_px = gutter * mult
82
+ diff = abs(gutter_px - pixels)
83
+ if diff < best_gutter_diff:
84
+ best_gutter_diff = diff
85
+ best_gutter = {"mult": mult, "px": gutter_px}
86
+
87
+ # Format multiplier: 0.5 -> "0.5", 1 -> "1", 1.5 -> "1.5", 2 -> "2"
88
+ mult = best_gutter["mult"]
89
+ if mult == int(mult):
90
+ mult_str = str(int(mult))
91
+ else:
92
+ mult_str = str(mult)
93
+
94
+ return {
95
+ "className": f"gutter-gap-{mult_str}",
96
+ "columns": 0,
97
+ "gutters": mult,
98
+ "actualWidth": round(best_gutter["px"]),
99
+ "pixelDifference": round(best_gutter["px"] - pixels),
100
+ "gridConfig": grid_config
101
+ }
102
+
103
+ # Find best matching span for values >= 1 column
104
+ best_match = None
105
+ best_diff = float('inf')
106
+ exact_match = False
107
+
108
+ for n in range(1, grid_columns + 1):
109
+ if exact_match:
110
+ break
111
+ for suffix in ["", "-wide", "-wider"]:
112
+ width = span_width(n, suffix)
113
+ diff = abs(width - pixels)
114
+ if diff < best_diff:
115
+ best_diff = diff
116
+ best_match = {
117
+ "columns": n,
118
+ "suffix": suffix,
119
+ "width": width
120
+ }
121
+ # Early exit if exact match
122
+ if diff == 0:
123
+ exact_match = True
124
+ break
125
+
126
+ class_name = f"span-w-{best_match['columns']}{best_match['suffix']}"
127
+ actual_width = best_match['width']
128
+
129
+ return {
130
+ "className": class_name,
131
+ "columns": best_match['columns'],
132
+ "actualWidth": round(actual_width),
133
+ "pixelDifference": round(actual_width - pixels),
134
+ "gridConfig": grid_config
135
+ }
136
+
137
+
138
+ def main():
139
+ parser = argparse.ArgumentParser(
140
+ description="Convert pixel values to Tailwind grid column classes",
141
+ formatter_class=argparse.RawDescriptionHelpFormatter,
142
+ epilog="""
143
+ Grid Presets:
144
+ Desktop: --columns 24 --mockup 1440 --gutter 24 --margin 24
145
+ Mobile: --columns 6 --mockup 375 --gutter 12 --margin 12
146
+
147
+ Examples:
148
+ %(prog)s 330 --columns 24 --mockup 1440 --gutter 24 --margin 24
149
+ %(prog)s 150 --columns 6 --mockup 375 --gutter 12 --margin 12
150
+ """
151
+ )
152
+
153
+ parser.add_argument("pixels", type=float, help="Pixel value to convert")
154
+ parser.add_argument("--columns", type=int, default=24, help="Number of grid columns (default: 24)")
155
+ parser.add_argument("--mockup", type=int, default=1440, help="Mockup width in pixels (default: 1440)")
156
+ parser.add_argument("--gutter", type=int, default=24, help="Gutter size in pixels (default: 24)")
157
+ parser.add_argument("--margin", type=int, default=24, help="Outer margin in pixels (default: 24)")
158
+ parser.add_argument("--json", action="store_true", help="Output full JSON result")
159
+
160
+ args = parser.parse_args()
161
+
162
+ if args.pixels <= 0:
163
+ print("Error: pixels must be positive", file=sys.stderr)
164
+ sys.exit(1)
165
+
166
+ result = pixels_to_columns(
167
+ pixels=args.pixels,
168
+ grid_columns=args.columns,
169
+ mockup_width=args.mockup,
170
+ gutter=args.gutter,
171
+ margin=args.margin
172
+ )
173
+
174
+ if args.json:
175
+ print(json.dumps(result, indent=2))
176
+ else:
177
+ # Concise output for quick use
178
+ diff = result["pixelDifference"]
179
+ diff_str = f"+{diff}px" if diff > 0 else f"{diff}px" if diff < 0 else "exact"
180
+ print(f"{result['className']} ({result['actualWidth']}px, {diff_str})")
181
+
182
+
183
+ if __name__ == "__main__":
184
+ main()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@numbered/docs-to-context",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "Generate project docs (component APIs, design system, architecture) and inject index into CLAUDE.md",
5
5
  "bin": {
6
6
  "docs-to-context": "scripts/generate.ts"
@@ -8,6 +8,7 @@
8
8
  "files": [
9
9
  "scripts/*.ts",
10
10
  "docs/**/*.mdx",
11
+ "docs/**/*.py",
11
12
  "README.md"
12
13
  ],
13
14
  "scripts": {
File without changes
package/scripts/inject.ts CHANGED
@@ -23,7 +23,7 @@ const MARKER_PATTERN = new RegExp(
23
23
  )
24
24
  const CORE_DOCS_ROOT = 'docs'
25
25
  const CONTEXT_ROOT = '.context'
26
- const REGENERATE_CMD = 'bunx @numbered/docs-to-context'
26
+ const REGENERATE_CMD = 'bunx @numbered/docs-to-context@latest'
27
27
 
28
28
  // ── Types ──────────────────────────────────────────────────────────────
29
29
 
@@ -32,6 +32,7 @@ interface CoreDocs {
32
32
  entities: string[]
33
33
  entitiesRoot: string
34
34
  bestPractices: string[]
35
+ layout: string[]
35
36
  }
36
37
 
37
38
  // ── Helpers ────────────────────────────────────────────────────────────
@@ -95,11 +96,48 @@ function copyBestPractices(projectRoot: string, platform: string): string[] {
95
96
  return copied
96
97
  }
97
98
 
99
+ // ── Layout docs copy ────────────────────────────────────────────────
100
+
101
+ function copyLayout(projectRoot: string): string[] {
102
+ const bundledDir = resolve(import.meta.dir, '..', 'docs', 'layout')
103
+ if (!isDir(bundledDir)) return []
104
+
105
+ const destDir = join(projectRoot, CONTEXT_ROOT, 'layout')
106
+ mkdirSync(destDir, { recursive: true })
107
+
108
+ const mdFiles = walkDir(bundledDir, ['.md', '.mdx'])
109
+ const copied: string[] = []
110
+
111
+ for (const src of mdFiles) {
112
+ const name = src.slice(src.lastIndexOf('/') + 1)
113
+ copyFileSync(src, join(destDir, name))
114
+ copied.push(`layout/${name}`)
115
+ }
116
+
117
+ // Copy scripts
118
+ const scriptsDir = join(bundledDir, 'scripts')
119
+ if (isDir(scriptsDir)) {
120
+ const destScriptsDir = join(projectRoot, CONTEXT_ROOT, 'scripts')
121
+ mkdirSync(destScriptsDir, { recursive: true })
122
+ const pyFiles = walkDir(scriptsDir, ['.py'])
123
+ for (const src of pyFiles) {
124
+ const name = src.slice(src.lastIndexOf('/') + 1)
125
+ copyFileSync(src, join(destScriptsDir, name))
126
+ }
127
+ }
128
+
129
+ if (copied.length) {
130
+ log.success(`Copied ${copied.length} layout doc(s)`)
131
+ }
132
+
133
+ return copied
134
+ }
135
+
98
136
  // ── Core docs discovery ────────────────────────────────────────────────
99
137
 
100
- function discoverCoreDocs(projectRoot: string, bestPractices: string[]): CoreDocs {
138
+ function discoverCoreDocs(projectRoot: string, bestPractices: string[], layout: string[]): CoreDocs {
101
139
  const docsDir = join(projectRoot, CORE_DOCS_ROOT)
102
- const result: CoreDocs = { frontend: [], entities: [], entitiesRoot: '', bestPractices }
140
+ const result: CoreDocs = { frontend: [], entities: [], entitiesRoot: '', bestPractices, layout }
103
141
 
104
142
  if (!isDir(docsDir)) return result
105
143
 
@@ -126,11 +164,16 @@ function buildComponentLines(names: string[]): string[] {
126
164
  `[Component Index]|root: ${root}`,
127
165
  '|IMPORTANT: Read component MDX before using any component',
128
166
  `|components:{${names.join(',')}}`,
129
- `|If ${root} is missing, run: ${REGENERATE_CMD}`,
167
+ `|If ./${CONTEXT_ROOT} is missing, run: ${REGENERATE_CMD}`,
130
168
  ]
131
169
  }
132
170
 
133
- function buildIndexBlock(indexPath: string, coreDocs: CoreDocs | null): string | null {
171
+ const PLATFORM_LABEL: Record<string, string> = {
172
+ nextjs: 'React & Next.js',
173
+ shopify: 'Liquid & Alpine.js',
174
+ }
175
+
176
+ function buildIndexBlock(indexPath: string, coreDocs: CoreDocs | null, platform?: string): string | null {
134
177
  const indexContent = readFileSync(indexPath, 'utf-8').trim()
135
178
 
136
179
  const names: string[] = []
@@ -147,17 +190,28 @@ function buildIndexBlock(indexPath: string, coreDocs: CoreDocs | null): string |
147
190
 
148
191
  // Frontend
149
192
  if (coreDocs?.frontend.length) {
150
- lines.push(`|[Frontend]|root: ${docsRoot}`)
193
+ lines.push(`|[Frontend — Design System & Grid]|root: ${docsRoot}`)
194
+ lines.push('|IMPORTANT: Read before any styling, layout, or spacing work')
151
195
  lines.push(`|frontend:{${coreDocs.frontend.join(',')}}`)
152
196
  }
153
197
 
154
198
  // Best Practices
155
199
  if (coreDocs?.bestPractices.length) {
156
200
  const contextRoot = `./${CONTEXT_ROOT}`
201
+ const label = (platform && PLATFORM_LABEL[platform]) || ''
157
202
  const fileNames = coreDocs.bestPractices.map((p) => p.slice(p.lastIndexOf('/') + 1))
158
- lines.push(`|[Best Practices]|root: ${contextRoot}`)
203
+ lines.push(`|[Best Practices${label ? ` — ${label}` : ''}]|root: ${contextRoot}`)
159
204
  lines.push(`|best-practices:{${fileNames.join(',')}}`)
160
- lines.push('|IMPORTANT: Read best-practices/README.mdx first it indexes all patterns by category and impact')
205
+ lines.push(`|IMPORTANT: ${label || 'Performance'} patterns — read best-practices/README.mdx first, it indexes all patterns by category and impact`)
206
+ }
207
+
208
+ // Layout
209
+ if (coreDocs?.layout.length) {
210
+ const contextRoot = `./${CONTEXT_ROOT}`
211
+ const fileNames = coreDocs.layout.map((p) => p.slice(p.lastIndexOf('/') + 1))
212
+ lines.push(`|[Layout & Integration]|root: ${contextRoot}`)
213
+ lines.push(`|IMPORTANT: Read before implementing any layout — convert pixels to columns with .context/scripts/pixels_to_columns.py`)
214
+ lines.push(`|layout:{${fileNames.join(',')}}`)
161
215
  }
162
216
 
163
217
  // Components (always present)
@@ -245,11 +299,14 @@ export function inject(opts: InjectOptions) {
245
299
  // Copy best practices if platform is known
246
300
  const bestPractices = opts.platform ? copyBestPractices(root, opts.platform) : []
247
301
 
248
- const coreDocs = discoverCoreDocs(root, bestPractices)
249
- const total = coreDocs.frontend.length + coreDocs.entities.length + coreDocs.bestPractices.length
302
+ // Copy layout docs (always)
303
+ const layout = copyLayout(root)
304
+
305
+ const coreDocs = discoverCoreDocs(root, bestPractices, layout)
306
+ const total = coreDocs.frontend.length + coreDocs.entities.length + coreDocs.bestPractices.length + coreDocs.layout.length
250
307
  if (total) log.info(`Found ${total} core doc(s)`)
251
308
 
252
- const block = buildIndexBlock(indexPath, total ? coreDocs : null)
309
+ const block = buildIndexBlock(indexPath, total ? coreDocs : null, opts.platform)
253
310
  if (!block) {
254
311
  log.error('INDEX file is empty, nothing to inject.')
255
312
  process.exit(1)