@numbered/docs-to-context 0.3.0 → 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.
- package/docs/layout/scripts/pixels_to_columns.py +184 -0
- package/package.json +2 -1
- package/scripts/generate.ts +0 -0
- package/scripts/inject.ts +75 -14
|
@@ -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
|
+
"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": {
|
package/scripts/generate.ts
CHANGED
|
File without changes
|
package/scripts/inject.ts
CHANGED
|
@@ -23,14 +23,16 @@ 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
|
|
|
30
30
|
interface CoreDocs {
|
|
31
31
|
frontend: string[]
|
|
32
32
|
entities: string[]
|
|
33
|
+
entitiesRoot: string
|
|
33
34
|
bestPractices: string[]
|
|
35
|
+
layout: string[]
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
// ── Helpers ────────────────────────────────────────────────────────────
|
|
@@ -94,11 +96,48 @@ function copyBestPractices(projectRoot: string, platform: string): string[] {
|
|
|
94
96
|
return copied
|
|
95
97
|
}
|
|
96
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
|
+
|
|
97
136
|
// ── Core docs discovery ────────────────────────────────────────────────
|
|
98
137
|
|
|
99
|
-
function discoverCoreDocs(projectRoot: string, bestPractices: string[]): CoreDocs {
|
|
138
|
+
function discoverCoreDocs(projectRoot: string, bestPractices: string[], layout: string[]): CoreDocs {
|
|
100
139
|
const docsDir = join(projectRoot, CORE_DOCS_ROOT)
|
|
101
|
-
const result: CoreDocs = { frontend: [], entities: [], bestPractices }
|
|
140
|
+
const result: CoreDocs = { frontend: [], entities: [], entitiesRoot: '', bestPractices, layout }
|
|
102
141
|
|
|
103
142
|
if (!isDir(docsDir)) return result
|
|
104
143
|
|
|
@@ -110,7 +149,8 @@ function discoverCoreDocs(projectRoot: string, bestPractices: string[]): CoreDoc
|
|
|
110
149
|
// Entities: architecture specs
|
|
111
150
|
const archDir = join(docsDir, 'specs', 'architecture')
|
|
112
151
|
if (isDir(archDir)) {
|
|
113
|
-
result.
|
|
152
|
+
result.entitiesRoot = relative(docsDir, archDir)
|
|
153
|
+
result.entities = walkDir(archDir, ['.md', '.mdx']).map((f) => relative(archDir, f))
|
|
114
154
|
}
|
|
115
155
|
|
|
116
156
|
return result
|
|
@@ -124,11 +164,16 @@ function buildComponentLines(names: string[]): string[] {
|
|
|
124
164
|
`[Component Index]|root: ${root}`,
|
|
125
165
|
'|IMPORTANT: Read component MDX before using any component',
|
|
126
166
|
`|components:{${names.join(',')}}`,
|
|
127
|
-
`|If
|
|
167
|
+
`|If ./${CONTEXT_ROOT} is missing, run: ${REGENERATE_CMD}`,
|
|
128
168
|
]
|
|
129
169
|
}
|
|
130
170
|
|
|
131
|
-
|
|
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 {
|
|
132
177
|
const indexContent = readFileSync(indexPath, 'utf-8').trim()
|
|
133
178
|
|
|
134
179
|
const names: string[] = []
|
|
@@ -145,17 +190,28 @@ function buildIndexBlock(indexPath: string, coreDocs: CoreDocs | null): string |
|
|
|
145
190
|
|
|
146
191
|
// Frontend
|
|
147
192
|
if (coreDocs?.frontend.length) {
|
|
148
|
-
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')
|
|
149
195
|
lines.push(`|frontend:{${coreDocs.frontend.join(',')}}`)
|
|
150
196
|
}
|
|
151
197
|
|
|
152
198
|
// Best Practices
|
|
153
199
|
if (coreDocs?.bestPractices.length) {
|
|
154
200
|
const contextRoot = `./${CONTEXT_ROOT}`
|
|
201
|
+
const label = (platform && PLATFORM_LABEL[platform]) || ''
|
|
155
202
|
const fileNames = coreDocs.bestPractices.map((p) => p.slice(p.lastIndexOf('/') + 1))
|
|
156
|
-
lines.push(`|[Best Practices]|root: ${contextRoot}`)
|
|
203
|
+
lines.push(`|[Best Practices${label ? ` — ${label}` : ''}]|root: ${contextRoot}`)
|
|
157
204
|
lines.push(`|best-practices:{${fileNames.join(',')}}`)
|
|
158
|
-
lines.push(
|
|
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(',')}}`)
|
|
159
215
|
}
|
|
160
216
|
|
|
161
217
|
// Components (always present)
|
|
@@ -163,9 +219,11 @@ function buildIndexBlock(indexPath: string, coreDocs: CoreDocs | null): string |
|
|
|
163
219
|
|
|
164
220
|
// Entities (grouped by directory)
|
|
165
221
|
if (coreDocs?.entities.length) {
|
|
166
|
-
|
|
222
|
+
const entRoot = coreDocs.entitiesRoot ? `${docsRoot}/${coreDocs.entitiesRoot}` : docsRoot
|
|
223
|
+
lines.push(`|[Entities]|root: ${entRoot}`)
|
|
167
224
|
for (const [dirPath, files] of groupByDir(coreDocs.entities)) {
|
|
168
|
-
|
|
225
|
+
const prefix = dirPath === '.' ? '' : `${dirPath}:`
|
|
226
|
+
lines.push(`|${prefix}{${files.join(',')}}`)
|
|
169
227
|
}
|
|
170
228
|
}
|
|
171
229
|
|
|
@@ -241,11 +299,14 @@ export function inject(opts: InjectOptions) {
|
|
|
241
299
|
// Copy best practices if platform is known
|
|
242
300
|
const bestPractices = opts.platform ? copyBestPractices(root, opts.platform) : []
|
|
243
301
|
|
|
244
|
-
|
|
245
|
-
const
|
|
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
|
|
246
307
|
if (total) log.info(`Found ${total} core doc(s)`)
|
|
247
308
|
|
|
248
|
-
const block = buildIndexBlock(indexPath, total ? coreDocs : null)
|
|
309
|
+
const block = buildIndexBlock(indexPath, total ? coreDocs : null, opts.platform)
|
|
249
310
|
if (!block) {
|
|
250
311
|
log.error('INDEX file is empty, nothing to inject.')
|
|
251
312
|
process.exit(1)
|