@tobi2409/layout 0.5.0 → 0.9.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/README.md CHANGED
@@ -1,117 +1,122 @@
1
1
  # Layout
2
2
 
3
- A small Delphi-like layout system based on CSS Grid.
3
+ A small build-time layout system based on CSS Grid. Frame children declare their position with an `@align` HTML attribute; the CLI parses the document and generates the required tracks and placements.
4
4
 
5
- The idea: a container with `.frame` defines fixed areas such as `top`, `left`, `client`, or `bottom`. Child elements are assigned to an area through the CSS custom property `--align`.
5
+ ## Installation
6
6
 
7
- ## Goal
7
+ ```sh
8
+ npm install @tobi2409/layout
9
+ ```
8
10
 
9
- The system is meant to enable simple, readable user interfaces without scattering many utility classes throughout the HTML.
11
+ ## Usage
10
12
 
11
- Example:
13
+ Add the `frame` class to a container and align its direct children:
12
14
 
13
15
  ```html
14
- <div class="frame">
15
- <header style="--align: top;">Header</header>
16
- <nav style="--align: left;">Navigation</nav>
17
- <main style="--align: client;">Inhalt</main>
18
- <aside style="--align: right;">Inspector</aside>
19
- <footer style="--align: bottom;">Footer</footer>
20
- </div>
16
+ <!doctype html>
17
+ <html lang="de">
18
+ <head>
19
+ <meta charset="utf-8">
20
+ <title>Application</title>
21
+ </head>
22
+ <body>
23
+ <div class="frame">
24
+ <header @align="top">Header</header>
25
+ <nav @align="left">Navigation</nav>
26
+ <main @align="client">Content</main>
27
+ <aside @align="right">Inspector</aside>
28
+ <footer @align="bottom">Status</footer>
29
+ </div>
30
+ </body>
31
+ </html>
21
32
  ```
22
33
 
23
- ## Usage
34
+ Process the HTML file:
24
35
 
25
- Include `layout.css`:
36
+ ```sh
37
+ npx layout path/to/application.html
38
+ ```
26
39
 
27
- ```html
28
- <link rel="stylesheet" href="layout.css">
40
+ The output directory defaults to `generated`. Use `--output-dir` (or `-o`) to choose another name:
41
+
42
+ ```sh
43
+ npx layout path/to/application.html --output-dir dist
29
44
  ```
30
45
 
31
- Then create a container with `.frame` and place its direct child elements using `--align`.
32
-
33
- ## Supported align values
34
-
35
- ### Horizontal / vertical
36
-
37
- - `top`
38
- - `top-2`
39
- - `top-3`
40
- - `top-4`
41
- - `top-5`
42
- - `left`
43
- - `left-2`
44
- - `left-3`
45
- - `left-4`
46
- - `left-5`
47
- - `client`
48
- - `right`
49
- - `right-2`
50
- - `right-3`
51
- - `right-4`
52
- - `right-5`
53
- - `bottom`
54
- - `bottom-2`
55
- - `bottom-3`
56
- - `bottom-4`
57
- - `bottom-5`
58
-
59
- `client` is the flexible main area and uses the remaining space.
60
-
61
- ## Basic principle
62
-
63
- - `top...` and `bottom...` occupy full rows.
64
- - `left...` and `right...` occupy columns next to the content.
65
- - `client` is placed in the center.
66
- - Multiple `.frame` containers can be nested.
67
-
68
- ## Nesting example
46
+ For `path/to/application.html`, the command leaves the source unchanged and writes these files:
47
+
48
+ ```text
49
+ path/to/generated/application.html
50
+ path/to/generated/application-layout.css
51
+ path/to/generated/application-layout.js
52
+ ```
53
+
54
+ The generated HTML contains `data-layout-*` attributes instead of `@align`, so it is valid HTML. It also receives the stylesheet and runtime references in its `head` unless they already exist:
69
55
 
70
56
  ```html
71
- <div class="frame" style="height: 300px;">
72
- <header style="--align: top;">Header</header>
73
-
74
- <main class="frame" style="--align: client;">
75
- <div style="--align: top;">Toolbar</div>
76
- <section style="--align: client;">Inhalt</section>
77
- <div style="--align: bottom;">Status</div>
78
- </main>
79
- </div>
57
+ <link rel="stylesheet" href="application-layout.css">
58
+ <script type="module" src="application-layout.js"></script>
80
59
  ```
81
60
 
82
- ## When this works well
61
+ Run the command again whenever the frame structure or an `@align` value changes. It overwrites both generated files deterministically without modifying the source document.
62
+
63
+ ## Alignments
64
+
65
+ - `top`: adds an `auto` row before the flexible content row
66
+ - `bottom`: adds an `auto` row after the flexible content row
67
+ - `left`: adds an `auto` column before the flexible content column
68
+ - `right`: adds an `auto` column after the flexible content column
69
+ - `client`: occupies the central `1fr` row and column
83
70
 
84
- Suitable for:
71
+ Repeated values are placed in document order. For example, three `top` children produce `auto auto auto 1fr`; numbered values such as `top-2` are no longer needed.
85
72
 
86
- - app layouts
87
- - admin interfaces
88
- - classic desktop-like UI structures
89
- - clearly separated areas such as header, sidebar, content, and footer
73
+ Only direct children of a frame are considered. Frames can be nested without their children affecting the parent frame.
90
74
 
91
- ## When to combine it with something else
75
+ ## Responsive alignment
92
76
 
93
- For dynamic lists or freely flowing rows, `.frame` is not the best level of abstraction. In those cases, a combination is usually better:
77
+ Use a JSON object for breakpoint-specific alignment. Because JSON uses double quotes, wrap the HTML attribute in single quotes:
94
78
 
95
- - `.frame` for the coarse page structure
96
- - Flexbox for lists, toolbars, or rows
97
- - `position: absolute` for overlays or freely positioned elements
79
+ ```html
80
+ <nav @align='{
81
+ "default": "left",
82
+ "max-width: 599px": "top",
83
+ "min-width: 1200px and max-height: 800px": "right"
84
+ }'>Navigation</nav>
85
+ ```
86
+
87
+ `default` is optional. Every other key becomes a media query. Feature lists can be written without surrounding parentheses; the generator emits valid CSS such as:
98
88
 
99
- ## Possible future direction
89
+ ```css
90
+ @media (min-width: 1200px) and (max-height: 800px) { /* ... */ }
91
+ ```
92
+
93
+ For each query, all tracks and placements in the affected frame are recalculated. When multiple media queries match simultaneously, normal CSS source order determines the result.
100
94
 
101
- It might be worth considering a switch from CSS Grid to Flexbox as the underlying model:
95
+ ## Runtime API
102
96
 
103
- - `top` / `bottom` → `flex-direction: column` (children stack vertically)
104
- - `left` / `right` → `flex-direction: row` (children stack horizontally)
97
+ The generated runtime exposes `setAlign` globally. Pass a generated layout element and its new alignment:
105
98
 
106
- This would naturally handle multiple children with the same alignment without overlapping, and without needing levels like `top-2` or `left-3`. Dynamic lists and reordering would also become easier. The trade-off is that the current single-class approach with `--align` would need to be rethought, since Flexbox distributes all children at once rather than placing them into named areas.
99
+ ```js
100
+ const inspector = document.querySelector(".inspector");
101
+ setAlign(inspector, "bottom");
102
+ ```
107
103
 
108
- ## Limitations
104
+ `setAlign` updates the element's alignment, recalculates its parent frame's `grid-template-rows` and `grid-template-columns`, and sets `grid-row` and `grid-column` for every aligned child. The runtime value overrides generated responsive placement for that frame until the page is reloaded.
109
105
 
110
- - Multiple elements with exactly the same `--align` overlap.
111
- - That is why there are levels such as `top-2` or `left-3`.
112
- - The number of levels is currently intentionally limited to 5 per direction.
106
+ The API can also be imported directly:
113
107
 
114
- ## Files
108
+ ```js
109
+ import { setAlign } from "@tobi2409/layout/runtime";
110
+ ```
111
+
112
+ Supported runtime values are `top`, `right`, `bottom`, `left`, and `client`. The element must be a direct generated child of a frame.
113
+
114
+ ## Development
115
+
116
+ ```sh
117
+ npm test
118
+ node bin/layout.mjs examples/demo/demo.html
119
+ node bin/layout.mjs examples/list/list.html
120
+ ```
115
121
 
116
- - [layout.css](layout.css): core of the layout system
117
- - [demo.html](demo.html): simple demo example
122
+ The Todo example in `examples/list/list.html` demonstrates how newly appended elements are registered and positioned with `setAlign(todo, "top")`.
package/bin/layout.mjs ADDED
@@ -0,0 +1,354 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { mkdir, readFile, writeFile } from 'node:fs/promises'
4
+ import { basename, dirname, extname, join } from 'node:path'
5
+ import { pathToFileURL } from 'node:url'
6
+ import { parse, serialize } from 'parse5'
7
+
8
+ const ALIGNMENTS = new Set(['top', 'right', 'bottom', 'left', 'client'])
9
+
10
+ function getAttribute(node, name) {
11
+ return node.attrs?.find((attribute) => attribute.name === name)
12
+ }
13
+
14
+ function setAttribute(node, name, value) {
15
+ const attribute = getAttribute(node, name)
16
+
17
+ if (attribute) {
18
+ attribute.value = value
19
+ } else {
20
+ node.attrs.push({ name, value })
21
+ }
22
+ }
23
+
24
+ function removeAttribute(node, name) {
25
+ node.attrs = node.attrs?.filter((attribute) => attribute.name !== name) ?? []
26
+ }
27
+
28
+ function hasClass(node, className) {
29
+ return getAttribute(node, 'class')?.value.split(/\s+/).includes(className) ?? false
30
+ }
31
+
32
+ function walk(node, visitor) {
33
+ // visitor ist ein Callback, der für jeden DOM-Knoten einmal aufgerufen wird.
34
+ visitor(node)
35
+
36
+ for (const child of node.childNodes ?? []) {
37
+ walk(child, visitor)
38
+ }
39
+ }
40
+
41
+ function parseAlignment(value, source) {
42
+ let parsed = value
43
+
44
+ if (value.trim().startsWith('{')) {
45
+ try {
46
+ parsed = JSON.parse(value)
47
+ } catch (error) {
48
+ throw new Error(`${source}: invalid JSON in @align: ${error.message}`)
49
+ }
50
+ }
51
+
52
+ if (typeof parsed === 'string') {
53
+ validateAlignment(parsed, source)
54
+ return { base: parsed, media: new Map() }
55
+ }
56
+
57
+ if (!parsed || Array.isArray(parsed) || typeof parsed !== 'object') {
58
+ throw new Error(`${source}: @align must be an alignment or a JSON object`);
59
+ }
60
+
61
+ const media = new Map()
62
+ let base
63
+
64
+ for (const [query, alignment] of Object.entries(parsed)) {
65
+ validateAlignment(alignment, source)
66
+
67
+ if (query === 'default') {
68
+ base = alignment
69
+ } else {
70
+ media.set(query, alignment)
71
+ }
72
+ }
73
+
74
+ return { base, media }
75
+ }
76
+
77
+ function validateAlignment(alignment, source) {
78
+ if (!ALIGNMENTS.has(alignment)) {
79
+ throw new Error(`${source}: unsupported alignment "${alignment}"`)
80
+ }
81
+ }
82
+
83
+ function repeat(track, count) {
84
+ return Array.from({ length: count }, () => track)
85
+ }
86
+
87
+ // Ermittelt für einen Frame die im Basis- oder Media-Query-Zustand wirksamen
88
+ // Alignments und erzeugt daraus CSS-Regeln für Grid-Tracks und Elementpositionen.
89
+ // Die Funktion wird beim Verarbeiten der Items einmal für den Basiszustand und
90
+ // zusätzlich für jede definierte Query aufgerufen. query ist dabei nur der Text
91
+ // des zu erzeugenden CSS-Zustands, nicht die aktuell passende Bildschirmgröße;
92
+ // ob eine Media Query zutrifft, entscheidet später der Browser.
93
+ function rulesForState(frameId, items, query) {
94
+ // Löst für jedes Element die aktuell gültige Ausrichtung auf: entweder den
95
+ // Wert der Media Query oder das Basis-Alignment. Elemente ohne wirksames
96
+ // Alignment werden ausgeschlossen.
97
+ const activeItems = items.flatMap((item) => {
98
+ const alignment = query && item.alignment.media.has(query)
99
+ ? item.alignment.media.get(query)
100
+ : item.alignment.base
101
+
102
+ // Beispiel: { id: "1-1", alignment: { base: "client", media: new Map() }, activeAlignment: "client" }
103
+ return alignment ? [{ ...item, activeAlignment: alignment }] : []
104
+ })
105
+
106
+ // Gruppiert die aktiven Elemente nach top, right, bottom, left und client.
107
+ // Die Gruppengrößen bestimmen die Anzahl der Grid-Tracks; die Position eines
108
+ // Elements in seiner Gruppe bestimmt anschließend grid-row bzw. grid-column.
109
+ const groups = Object.fromEntries(
110
+ [...ALIGNMENTS].map((alignment) => [
111
+ alignment,
112
+ activeItems.filter((item) => item.activeAlignment === alignment)
113
+ ])
114
+ )
115
+
116
+ const centerRow = groups.top.length + 1
117
+ const centerColumn = groups.left.length + 1
118
+
119
+ const rows = [...repeat('auto', groups.top.length), '1fr', ...repeat('auto', groups.bottom.length)]
120
+ const columns = [...repeat('auto', groups.left.length), '1fr', ...repeat('auto', groups.right.length)]
121
+
122
+ const rules = [
123
+ `[data-layout-frame='${frameId}'] {`,
124
+ ` grid-template-rows: ${rows.join(' ')};`,
125
+ ` grid-template-columns: ${columns.join(' ')};`,
126
+ "}"
127
+ ]
128
+
129
+ groups.top.forEach((item, index) => rules.push(itemRule(item.id, index + 1, '1 / -1')))
130
+ groups.bottom.forEach((item, index) => rules.push(itemRule(item.id, centerRow + index + 1, '1 / -1')))
131
+ groups.left.forEach((item, index) => rules.push(itemRule(item.id, centerRow, String(index + 1))))
132
+ groups.right.forEach((item, index) => rules.push(itemRule(item.id, centerRow, String(centerColumn + index + 1))))
133
+ groups.client.forEach((item) => rules.push(itemRule(item.id, centerRow, String(centerColumn))))
134
+
135
+ return rules.join('\n');
136
+ }
137
+
138
+ function itemRule(id, row, column) {
139
+ return `[data-layout-item='${id}'] { grid-row: ${row}; grid-column: ${column}; }`
140
+ }
141
+
142
+ function formatMediaQuery(query) {
143
+ const normalized = query.trim().replace(/\s+an\s+/gi, ' and ')
144
+
145
+ if (normalized.startsWith('(')) {
146
+ return normalized
147
+ }
148
+
149
+ return normalized
150
+ .split(/\s+and\s+/i)
151
+ .map((feature) => `(${feature.trim()})`)
152
+ .join(' and ')
153
+ }
154
+
155
+ // Stellt sicher, dass im <head> genau ein Link auf das generierte Stylesheet
156
+ // vorhanden ist. Ein bereits existierender Link wird nicht erneut eingefügt.
157
+ function ensureStylesheet(document, href) {
158
+ let head
159
+ let existing
160
+
161
+ walk(document, (node) => {
162
+ if (node.tagName === 'head') {
163
+ head = node
164
+ }
165
+
166
+ if (node.tagName === 'link' && getAttribute(node, 'href')?.value === href) {
167
+ existing = node;
168
+ }
169
+ })
170
+
171
+ if (existing) {
172
+ return
173
+ }
174
+
175
+ if (!head) {
176
+ throw new Error('HTML document has no <head> element')
177
+ }
178
+
179
+ const fragment = parse(`<link rel="stylesheet" href="${href}">`)
180
+ let link
181
+
182
+ // parse gibt ein DOM-Fragment zurück, daher müssen wir das <link>-Element daraus extrahieren.
183
+ walk(fragment, (node) => {
184
+ if (node.tagName === 'link') {
185
+ link = node
186
+ }
187
+ })
188
+
189
+ link.parentNode = head
190
+ head.childNodes.push(link)
191
+ }
192
+
193
+ // Stellt sicher, dass im <head> genau ein Modul-Script für die Runtime-API
194
+ // vorhanden ist. Ein bereits existierendes Script wird nicht erneut eingefügt.
195
+ function ensureRuntime(document, src) {
196
+ let head
197
+ let existing
198
+
199
+ walk(document, (node) => {
200
+ if (node.tagName === 'head') {
201
+ head = node
202
+ }
203
+
204
+ if (node.tagName === 'script' && getAttribute(node, 'src')?.value === src) {
205
+ existing = node
206
+ }
207
+ })
208
+
209
+ if (existing) {
210
+ return
211
+ }
212
+
213
+ if (!head) {
214
+ throw new Error('HTML document has no <head> element')
215
+ }
216
+
217
+ const fragment = parse(`<script type="module" src="${src}"></script>`)
218
+ let script
219
+
220
+ walk(fragment, (node) => {
221
+ if (node.tagName === 'script') {
222
+ script = node
223
+ }
224
+ })
225
+
226
+ script.parentNode = head
227
+ head.childNodes.push(script)
228
+ }
229
+
230
+ export function compileHtml(source, sourceName = 'document.html') {
231
+ const document = parse(source)
232
+ const frames = []
233
+
234
+ walk(document, (node) => {
235
+ if (node.tagName && hasClass(node, 'frame')) {
236
+ frames.push(node)
237
+ }
238
+ })
239
+
240
+ const css = [
241
+ '.frame {',
242
+ ' position: relative;',
243
+ ' display: grid;',
244
+ '}'
245
+ ]
246
+
247
+ frames.forEach((frame, frameIndex) => {
248
+ const frameId = String(frameIndex + 1)
249
+ setAttribute(frame, 'data-layout-frame', frameId)
250
+
251
+ const items = []
252
+
253
+ for (const child of frame.childNodes ?? []) {
254
+ const attribute = getAttribute(child, '@align')
255
+
256
+ if (!attribute) {
257
+ continue
258
+ }
259
+
260
+ const id = `${frameId}-${items.length + 1}`
261
+
262
+ setAttribute(child, 'data-layout-item', id)
263
+ setAttribute(child, 'data-layout-align', attribute.value)
264
+
265
+ // alignment kommt nicht aus dem DOM: parseAlignment wandelt den rohen
266
+ // @align-Attributwert in { base, media } für die CSS-Erzeugung um.
267
+ items.push({ id, alignment: parseAlignment(attribute.value, sourceName) })
268
+ removeAttribute(child, '@align')
269
+ }
270
+
271
+ css.push('', rulesForState(frameId, items))
272
+
273
+ const queries = new Set(items.flatMap((item) => [...item.alignment.media.keys()]))
274
+
275
+ for (const query of queries) {
276
+ const mediaRules = rulesForState(frameId, items, query)
277
+ .split('\n')
278
+ .map((line) => ` ${line}`)
279
+ .join('\n')
280
+
281
+ css.push('', `@media ${formatMediaQuery(query)} {\n${mediaRules}\n}`)
282
+ }
283
+ })
284
+
285
+ const extension = extname(sourceName)
286
+ const stem = basename(sourceName, extension)
287
+ const stylesheet = `${stem}-layout.css`
288
+ const runtime = `${stem}-layout.js`
289
+
290
+ ensureStylesheet(document, stylesheet)
291
+ ensureRuntime(document, runtime)
292
+
293
+ return { html: serialize(document), css: `${css.join('\n')}\n`, stylesheet, runtime }
294
+ }
295
+
296
+ export async function compileFile(fileName, outputDirectoryName = 'generated') {
297
+ const source = await readFile(fileName, 'utf8')
298
+ const result = compileHtml(source, fileName)
299
+ const outputDirectory = join(dirname(fileName), outputDirectoryName)
300
+ const htmlFile = join(outputDirectory, basename(fileName))
301
+ const cssFile = join(outputDirectory, result.stylesheet)
302
+ const runtimeFile = join(outputDirectory, result.runtime)
303
+ const runtimeSource = await readFile(new URL('../src/layout.js', import.meta.url), 'utf8')
304
+
305
+ await mkdir(outputDirectory, { recursive: true })
306
+
307
+ await Promise.all([
308
+ writeFile(htmlFile, result.html),
309
+ writeFile(cssFile, result.css),
310
+ writeFile(runtimeFile, runtimeSource)
311
+ ])
312
+
313
+ return { ...result, htmlFile, cssFile, runtimeFile }
314
+ }
315
+
316
+ async function main() {
317
+ const args = process.argv.slice(2)
318
+ const fileName = args[0]
319
+ let outputDirectoryName = 'generated'
320
+
321
+ if (!fileName) {
322
+ console.error('Usage: layout <document.html> [--output-dir <name>]')
323
+ process.exitCode = 1
324
+ return
325
+ }
326
+
327
+ for (let index = 1; index < args.length; index += 1) {
328
+ const argument = args[index]
329
+
330
+ if (argument === '--output-dir' || argument === '-o') {
331
+ outputDirectoryName = args[index + 1]
332
+ index += 1
333
+
334
+ if (!outputDirectoryName) {
335
+ throw new Error(`${argument} requires a directory name`)
336
+ }
337
+ } else {
338
+ throw new Error(`Unknown argument: ${argument}`)
339
+ }
340
+ }
341
+
342
+ const result = await compileFile(fileName, outputDirectoryName)
343
+
344
+ console.log(`Generated ${result.htmlFile}`)
345
+ console.log(`Generated ${result.cssFile}`)
346
+ console.log(`Generated ${result.runtimeFile}`)
347
+ }
348
+
349
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
350
+ main().catch((error) => {
351
+ console.error(error.message)
352
+ process.exitCode = 1
353
+ })
354
+ }
package/package.json CHANGED
@@ -1,14 +1,23 @@
1
1
  {
2
2
  "name": "@tobi2409/layout",
3
- "version": "0.5.0",
4
- "description": "Tiny CSS frame layout using named grid areas and the --align property",
3
+ "version": "0.9.0",
4
+ "description": "Build-time HTML frame layout generator using CSS Grid",
5
5
  "license": "MIT",
6
- "main": "src/layout.css",
6
+ "type": "module",
7
+ "exports": {
8
+ "./runtime": "./src/layout.js"
9
+ },
10
+ "bin": {
11
+ "layout": "bin/layout.mjs"
12
+ },
7
13
  "files": [
14
+ "bin",
8
15
  "src",
9
16
  "README.md"
10
17
  ],
11
- "style": "src/layout.css",
18
+ "scripts": {
19
+ "test": "node --test"
20
+ },
12
21
  "keywords": [
13
22
  "css",
14
23
  "css-library",
@@ -26,8 +35,8 @@
26
35
  "frontend",
27
36
  "responsive",
28
37
  "align",
29
- "grid-area",
30
- "named-areas",
38
+ "html-attributes",
39
+ "build-time",
31
40
  "client-area",
32
41
  "top",
33
42
  "bottom",
@@ -40,5 +49,8 @@
40
49
  ],
41
50
  "publishConfig": {
42
51
  "access": "public"
52
+ },
53
+ "dependencies": {
54
+ "parse5": "^8.0.1"
43
55
  }
44
- }
56
+ }
package/src/layout.js ADDED
@@ -0,0 +1,96 @@
1
+ const ALIGNMENTS = new Set(['top', 'right', 'bottom', 'left', 'client'])
2
+ let runtimeItemId = 0
3
+
4
+ function validateAlignment(alignment) {
5
+ if (!ALIGNMENTS.has(alignment)) {
6
+ throw new TypeError(`Unsupported alignment "${alignment}"`)
7
+ }
8
+ }
9
+
10
+ function formatMediaQuery(query) {
11
+ const normalized = query.trim().replace(/\s+an\s+/gi, ' and ')
12
+
13
+ if (normalized.startsWith('(')) {
14
+ return normalized
15
+ }
16
+
17
+ return normalized
18
+ .split(/\s+and\s+/i)
19
+ .map((feature) => `(${feature.trim()})`)
20
+ .join(' and ')
21
+ }
22
+
23
+ function getAlignment(element) {
24
+ const value = element.dataset.layoutAlign
25
+
26
+ if (!value?.trim().startsWith('{')) {
27
+ return value
28
+ }
29
+
30
+ const specification = JSON.parse(value)
31
+ let alignment = specification.default
32
+
33
+ for (const [query, candidate] of Object.entries(specification)) {
34
+ if (query !== 'default' && globalThis.matchMedia(formatMediaQuery(query)).matches) {
35
+ alignment = candidate
36
+ }
37
+ }
38
+
39
+ return alignment
40
+ }
41
+
42
+ function place(element, row, column) {
43
+ element.style.gridRow = String(row)
44
+ element.style.gridColumn = String(column)
45
+ }
46
+
47
+ function updateFrame(frame) {
48
+ const groups = Object.fromEntries([...ALIGNMENTS].map((alignment) => [alignment, []]))
49
+ const items = [...frame.children].filter((child) => child.dataset.layoutItem && child.dataset.layoutAlign)
50
+
51
+ for (const item of items) {
52
+ const alignment = getAlignment(item)
53
+
54
+ if (alignment) {
55
+ validateAlignment(alignment)
56
+ groups[alignment].push(item)
57
+ }
58
+
59
+ item.style.gridRow = ''
60
+ item.style.gridColumn = ''
61
+ }
62
+
63
+ const centerRow = groups.top.length + 1
64
+ const centerColumn = groups.left.length + 1
65
+
66
+ frame.style.gridTemplateRows = [...groups.top.map(() => 'auto'), '1fr', ...groups.bottom.map(() => 'auto')].join(' ')
67
+ frame.style.gridTemplateColumns = [...groups.left.map(() => 'auto'), '1fr', ...groups.right.map(() => 'auto')].join(' ')
68
+
69
+ groups.top.forEach((item, index) => place(item, index + 1, '1 / -1'))
70
+ groups.bottom.forEach((item, index) => place(item, centerRow + index + 1, '1 / -1'))
71
+ groups.left.forEach((item, index) => place(item, centerRow, index + 1))
72
+ groups.right.forEach((item, index) => place(item, centerRow, centerColumn + index + 1))
73
+ groups.client.forEach((item) => place(item, centerRow, centerColumn))
74
+ }
75
+
76
+ export function setAlign(element, alignment) {
77
+ validateAlignment(alignment)
78
+
79
+ const frame = element?.parentElement
80
+
81
+ if (!frame?.dataset.layoutFrame) {
82
+ throw new TypeError("setAlign expects a direct child of a generated frame")
83
+ }
84
+
85
+ if (!element.dataset.layoutItem) {
86
+ runtimeItemId += 1
87
+ element.dataset.layoutItem = `${frame.dataset.layoutFrame}-runtime-${runtimeItemId}`
88
+ }
89
+
90
+ element.dataset.layoutAlign = alignment
91
+ updateFrame(frame)
92
+
93
+ return element
94
+ }
95
+
96
+ globalThis.setAlign = setAlign
package/src/layout.css DELETED
@@ -1,24 +0,0 @@
1
- .frame {
2
- position: relative;
3
- display: grid;
4
- grid-template-rows: auto auto auto auto auto 1fr auto auto auto auto auto; /* top / client / bottom */
5
- grid-template-columns: auto auto auto auto auto 1fr auto auto auto auto auto; /* left / client / right */
6
- grid-template-areas:
7
- "top top top top top top top top top top top"
8
- "top-2 top-2 top-2 top-2 top-2 top-2 top-2 top-2 top-2 top-2 top-2"
9
- "top-3 top-3 top-3 top-3 top-3 top-3 top-3 top-3 top-3 top-3 top-3"
10
- "top-4 top-4 top-4 top-4 top-4 top-4 top-4 top-4 top-4 top-4 top-4"
11
- "top-5 top-5 top-5 top-5 top-5 top-5 top-5 top-5 top-5 top-5 top-5"
12
- "left left-2 left-3 left-4 left-5 client right right-2 right-3 right-4 right-5"
13
- "bottom bottom bottom bottom bottom bottom bottom bottom bottom bottom bottom"
14
- "bottom-2 bottom-2 bottom-2 bottom-2 bottom-2 bottom-2 bottom-2 bottom-2 bottom-2 bottom-2 bottom-2"
15
- "bottom-3 bottom-3 bottom-3 bottom-3 bottom-3 bottom-3 bottom-3 bottom-3 bottom-3 bottom-3 bottom-3"
16
- "bottom-4 bottom-4 bottom-4 bottom-4 bottom-4 bottom-4 bottom-4 bottom-4 bottom-4 bottom-4 bottom-4"
17
- "bottom-5 bottom-5 bottom-5 bottom-5 bottom-5 bottom-5 bottom-5 bottom-5 bottom-5 bottom-5 bottom-5";
18
- }
19
-
20
- .frame > * {
21
- grid-area: var(--align, auto);
22
- }
23
-
24
- /* soll sich Element frei im Parent bewegen, einfach absolute Positionierung */