@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.
- 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 +68 -11
|
@@ -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,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
|
|
167
|
+
`|If ./${CONTEXT_ROOT} is missing, run: ${REGENERATE_CMD}`,
|
|
130
168
|
]
|
|
131
169
|
}
|
|
132
170
|
|
|
133
|
-
|
|
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(
|
|
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
|
-
|
|
249
|
-
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
|
|
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)
|