@ojiepermana/angular-theme 22.0.36 → 22.0.43

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.
Files changed (46) hide show
  1. package/README.md +25 -12
  2. package/fesm2022/ojiepermana-angular-theme-layout-types.mjs +10 -1
  3. package/fesm2022/ojiepermana-angular-theme-layout-wrapper.mjs +72 -22
  4. package/fesm2022/ojiepermana-angular-theme-layout.mjs +117 -45
  5. package/fesm2022/ojiepermana-angular-theme-page.mjs +251 -101
  6. package/fesm2022/ojiepermana-angular-theme-styles.mjs +19 -0
  7. package/layout/README.md +41 -36
  8. package/package.json +3 -3
  9. package/page/README.md +53 -15
  10. package/styles/README.md +11 -3
  11. package/styles/css/base/components.css +212 -0
  12. package/styles/css/base/theme.css +117 -49
  13. package/styles/css/base/tokens.css +98 -48
  14. package/styles/css/color/amber.css +2 -0
  15. package/styles/css/color/blue.css +2 -0
  16. package/styles/css/color/cyan.css +2 -0
  17. package/styles/css/color/emerald.css +2 -0
  18. package/styles/css/color/fuchsia.css +2 -0
  19. package/styles/css/color/green.css +2 -0
  20. package/styles/css/color/indigo.css +2 -0
  21. package/styles/css/color/lime.css +2 -0
  22. package/styles/css/color/orange.css +2 -0
  23. package/styles/css/color/pink.css +2 -0
  24. package/styles/css/color/purple.css +2 -0
  25. package/styles/css/color/red.css +2 -0
  26. package/styles/css/color/rose.css +2 -0
  27. package/styles/css/color/sky.css +2 -0
  28. package/styles/css/color/teal.css +2 -0
  29. package/styles/css/color/violet.css +2 -0
  30. package/styles/css/color/yellow.css +2 -0
  31. package/styles/css/neutral/gray.css +2 -0
  32. package/styles/css/neutral/mauve.css +2 -0
  33. package/styles/css/neutral/mist.css +2 -0
  34. package/styles/css/neutral/neutral.css +2 -0
  35. package/styles/css/neutral/olive.css +2 -0
  36. package/styles/css/neutral/slate.css +2 -0
  37. package/styles/css/neutral/stone.css +2 -0
  38. package/styles/css/neutral/taupe.css +2 -0
  39. package/styles/css/neutral/zinc.css +2 -0
  40. package/styles/css/space/index.css +6 -3
  41. package/types/ojiepermana-angular-theme-layout-services.d.ts +1 -1
  42. package/types/ojiepermana-angular-theme-layout-types.d.ts +1 -1
  43. package/types/ojiepermana-angular-theme-layout-wrapper.d.ts +40 -7
  44. package/types/ojiepermana-angular-theme-layout.d.ts +4 -2
  45. package/types/ojiepermana-angular-theme-page.d.ts +88 -36
  46. package/types/ojiepermana-angular-theme-styles.d.ts +1 -0
package/layout/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @ojiepermana/angular-theme/layout
2
2
 
3
- Primitive layout projection untuk menyusun shell UI dengan satu root `Layout`, satu variant layout (`vertical`, `horizontal`, `empty`, atau `fluid`), slot `layout-nav`, dan area `LayoutContent` yang menjadi scroll container utama.
3
+ Primitive layout projection untuk menyusun shell UI dengan satu root `Layout`, satu variant layout (`vertical`, `horizontal`, `empty`, atau `fluid`), slot `LayoutNavigation`, dan area `LayoutContent` yang menjadi scroll container utama.
4
4
 
5
5
  README ini mendokumentasikan seluruh API publik yang diekspor oleh package agar consumer bisa memahami kontrak komponen, type, service, dan perilaku layout tanpa perlu membaca source code.
6
6
 
@@ -65,17 +65,17 @@ Struktur yang direkomendasikan adalah sebagai berikut.
65
65
 
66
66
  ```html
67
67
  <Layout>
68
- <LayoutVertical | layout-horizontal | layout-empty | layout-fluid>
68
+ <LayoutVertical | LayoutHorizontal | LayoutEmpty | LayoutFluid>
69
69
  <LayoutNavigation>...</LayoutNavigation>
70
70
  <LayoutContent>...</LayoutContent>
71
- </layout-vertical | layout-horizontal | layout-empty | layout-fluid>
71
+ </LayoutVertical | LayoutHorizontal | LayoutEmpty | LayoutFluid>
72
72
  </Layout>
73
73
  ```
74
74
 
75
75
  Aturan penggunaannya:
76
76
 
77
77
  - Gunakan tepat satu variant layout di dalam `Layout`.
78
- - Untuk layout `vertical` dan `horizontal`, urutan child yang umum adalah `layout-nav` lalu `LayoutContent`.
78
+ - Untuk layout `vertical` dan `horizontal`, urutan child yang umum adalah `LayoutNavigation` lalu `LayoutContent`.
79
79
  - Untuk layout `empty`, biasanya cukup `LayoutContent`.
80
80
  - Untuk layout `fluid`, child dapat berupa konten page tunggal yang ingin dipusatkan terhadap frame.
81
81
  - `LayoutContent` adalah area yang memiliki `overflow-auto`, jadi konten utama sebaiknya ditempatkan di sana.
@@ -194,13 +194,14 @@ Selector: `Layout`
194
194
 
195
195
  Inputs:
196
196
 
197
- | Input | Type | Default | Deskripsi |
198
- | ------------------- | -------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
199
- | `surface` | `LayoutSurface` | `'flat'` | Menentukan fallback background root layout. Jika local storage `layout-surface` berisi nilai valid, nilai storage yang dipakai. |
200
- | `appearance` | `LayoutAppearance or null` | `null` | API template untuk menentukan fallback appearance frame. Jika input ini kosong, primitive mencoba alias `layout-appearance`, lalu fallback efektif akhirnya `flat`. |
201
- | `layout-appearance` | `LayoutAppearance or null` | `null` | Alias simetris dengan `nav-appearance` pada navigation (kosakata sama `flat \| border-rail`). Memberi `appearance` yang sama ke shell & nav membuat keduanya seragam. |
202
- | `width` | `LayoutWidth` | `'full'` | Menentukan fallback padding outer dan perilaku container frame. Jika local storage `layout-width` berisi nilai valid, nilai storage yang dipakai. |
203
- | `class` | `string` | `''` | Menambahkan class pada host `Layout`. |
197
+ | Input | Type | Default | Deskripsi |
198
+ | ------------------- | -------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
199
+ | `surface` | `LayoutSurface` | `'flat'` | Menentukan fallback background root layout. Jika local storage `layout-surface` berisi nilai valid, nilai storage yang dipakai. |
200
+ | `appearance` | `LayoutAppearance or null` | `null` | API template untuk menentukan fallback appearance frame. Jika input ini kosong, primitive mencoba alias `layout-appearance`, lalu fallback efektif akhirnya `flat`. |
201
+ | `layout-appearance` | `LayoutAppearance or null` | `null` | Alias simetris dengan `nav-appearance` pada navigation (kosakata sama `flat \| border-rail`). Memberi `appearance` yang sama ke shell & nav membuat keduanya seragam. |
202
+ | `width` | `LayoutWidth` | `'full'` | Menentukan fallback padding outer dan perilaku container frame. Jika local storage `layout-width` berisi nilai valid, nilai storage yang dipakai. |
203
+ | `layout-type` | `LayoutType or null` | `null` | Override eksplisit type aktif (`vertical \| horizontal \| empty \| fluid`). Jika di-set, menjadi sumber kebenaran type dan menimpa variant `Layout*`. Jika `null`, variant yang menetapkan. |
204
+ | `class` | `string` | `''` | Menambahkan class pada host `Layout`. |
204
205
 
205
206
  Behavior:
206
207
 
@@ -208,7 +209,7 @@ Behavior:
208
209
  - Jika input manual kosong, primitive mendaftarkan default `surface`, `appearance`, dan `width` ke `LayoutService` hanya saat local storage belum memiliki nilai valid.
209
210
  - Nilai visual final selalu dibaca kembali dari `LayoutService`, sehingga child primitive tetap membaca state yang sama selama instance aktif.
210
211
  - Menambahkan atribut host `data-surface`, `data-layout-appearance`, `data-layout-width`, dan `data-layout-type`.
211
- - Root tidak menyediakan input `type`. Type aktif dikendalikan oleh variant layout yang dirender sebagai override in-memory, atau oleh consumer melalui `LayoutService.registerDefaults({ type })` sebelum template mengevaluasi `layout.type()`.
212
+ - Root menyediakan input opsional `layout-type`. Bila di-set, input ini menjadi sumber kebenaran type aktif dan menimpa variant layout yang dirender (override in-memory tanpa menulis `localStorage`). Bila `null`, type aktif dikendalikan oleh variant layout yang dirender, atau oleh consumer melalui `LayoutService.registerDefaults({ type })` sebelum template mengevaluasi `layout.type()`.
212
213
  - Selalu merender frame border.
213
214
  - Jika `appearance="border-rail"`, root menambah rail dekoratif di empat sudut frame, rail inset horizontal, dan rail vertikal sekunder di luar sisi kiri-kanan frame sehingga frame terlihat seperti memiliki double rail.
214
215
  - Jika `width="container"`, frame dipusatkan mulai breakpoint `lg` dengan container behavior.
@@ -266,20 +267,21 @@ Behavior:
266
267
 
267
268
  - Mengatur `LayoutService.type` menjadi `'empty'` hanya untuk state aktif dan tidak menulis `localStorage`.
268
269
  - Menyediakan wrapper penuh untuk satu area konten.
269
- - `layout-nav` akan tersembunyi jika tetap dirender di mode ini.
270
+ - `LayoutNavigation` akan tersembunyi jika tetap dirender di mode ini.
270
271
 
271
272
  ### `LayoutNavigationComponent`
272
273
 
273
274
  Wrapper untuk slot navigasi.
274
275
 
275
- Selector: `layout-nav`
276
+ Selector: `LayoutNavigation`
276
277
 
277
278
  Inputs:
278
279
 
279
- | Input | Type | Default | Deskripsi |
280
- | ----------- | -------- | --------------------- | ------------------------------------------------ |
281
- | `ariaLabel` | `string` | `'Layout navigation'` | Label aksesibilitas untuk landmark `navigation`. |
282
- | `class` | `string` | `''` | Menambahkan class pada host `layout-nav`. |
280
+ | Input | Type | Default | Deskripsi |
281
+ | ------------ | ---------------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
282
+ | `ariaLabel` | `string` | `'Layout navigation'` | Label aksesibilitas untuk landmark `navigation`. |
283
+ | `railOffset` | `string \| null` | `null` | Override offset rail vertikal nav (mode `vertical + border-rail`). Jika `null`, mengikuti preview rail offset dari nav yang diproyeksikan. |
284
+ | `class` | `string` | `''` | Menambahkan class pada host `LayoutNavigation`. |
283
285
 
284
286
  Behavior:
285
287
 
@@ -293,7 +295,7 @@ Behavior:
293
295
  Catatan penggunaan:
294
296
 
295
297
  - Untuk mode `border-rail`, jangan tambahkan `border-r` manual pada konten nav bila yang diinginkan adalah rail bawaan primitive.
296
- - Styling visual isi nav sebaiknya ditempatkan pada elemen child di dalam `layout-nav`, bukan dengan mengandalkan border host.
298
+ - Styling visual isi nav sebaiknya ditempatkan pada elemen child di dalam `LayoutNavigation`, bukan dengan mengandalkan border host.
297
299
 
298
300
  ### `LayoutContentComponent`
299
301
 
@@ -322,8 +324,11 @@ Nilai `surface` yang tersedia berasal dari `LAYOUT_SURFACES`.
322
324
  | Nilai | Efek visual |
323
325
  | ------------------- | -------------------------------------------------------------------------- |
324
326
  | `'flat'` | Background polos `bg-background`. |
325
- | `'grid'` | Pola grid tipis dua arah. |
326
- | `'honeycome'` | Pola radial rapat. Nama API dieja `honeycome` agar sesuai export saat ini. |
327
+ | `'grid'` | Pola grid tipis dua arah; warna garis (`--layout-grid-color`) mengikuti Accent dengan kecerahan tetap (fallback neutral). |
328
+ | `'grid-line'` | Mosaik kotak: grid tipis + kotak highlight menyala. Mengikuti warna Accent (hue dari `--accent-foreground`, kecerahan dikunci `--layout-grid-highlight-l`), invert light/dark. Memaksa sudut frame kotak (`rounded-none`, seperti `border-rail`) + border frame warna Accent. Pada appearance `border-rail`, garis rail + border frame (tebal `--layout-rail-width` = 1.21px) ikut warna Accent (`--layout-rail-color`) dengan kecerahan dinaikkan (`--layout-rail-opacity`), bukan `--border` yang soft. |
329
+ | `'honeycomb'` | Pola heksagon honeycomb tipis (outline), token-colored mengikuti tema. |
330
+ | `'matrix'` | Field biner (0/1) acak tipis, tinted brand `--primary`, recolor mengikuti tema. |
331
+ | `'circuit'` | Tekstur PCB / papan sirkuit yang ter-tile: jalur (trace) berbelok 45°, node via, dan pad cincin konsentris. Dua lapisan mask — trace tembaga netral (`--layout-grid-line`) + node aksen brand (`--primary`) — jadi papannya grayscale tapi node-nya menyala mengikuti tema; recolor light/dark. |
327
332
  | `'line-vertical'` | Garis vertikal berulang. |
328
333
  | `'line-horizontal'` | Garis horizontal berulang. |
329
334
 
@@ -429,27 +434,27 @@ Seluruh symbol berikut diekspor dari `@ojiepermana/angular-theme/layout/types`.
429
434
  | Symbol | Nilai |
430
435
  | -------------------- | -------------------------------------------------------------------------------------------------- |
431
436
  | `LayoutType` | `vertical`, `horizontal`, `empty`, `fluid` |
432
- | `LayoutSurface` | `flat`, `grid`, `honeycome`, `line-vertical`, `line-horizontal` |
437
+ | `LayoutSurface` | `flat`, `grid`, `grid-line`, `honeycomb`, `matrix`, `circuit`, `line-vertical`, `line-horizontal` |
433
438
  | `LayoutAppearance` | `flat`, `border-rail` |
434
439
  | `LayoutWidth` | `full`, `wide`, `container`, `fluid` |
435
440
  | `LayoutContextValue` | Objekt berbentuk `{ surface, type, appearance, width }` yang masing-masing berupa readonly signal. |
436
441
 
437
442
  ### Constants
438
443
 
439
- | Symbol | Nilai |
440
- | ------------------------------- | ------------------------------------------------------------------- |
441
- | `LAYOUT_TYPES` | `['vertical', 'horizontal', 'empty', 'fluid']` |
442
- | `LAYOUT_SURFACES` | `['flat', 'grid', 'honeycome', 'line-vertical', 'line-horizontal']` |
443
- | `LAYOUT_APPEARANCES` | `['flat', 'border-rail']` |
444
- | `LAYOUT_WIDTHS` | `['full', 'wide', 'container', 'fluid']` |
445
- | `LAYOUT_DEFAULT_SURFACE` | `'flat'` |
446
- | `LAYOUT_DEFAULT_TYPE` | `'vertical'` |
447
- | `LAYOUT_DEFAULT_APPEARANCE` | `'flat'` |
448
- | `LAYOUT_DEFAULT_WIDTH` | `'full'` |
449
- | `LAYOUT_SURFACE_STORAGE_KEY` | `'layout-surface'` |
450
- | `LAYOUT_APPEARANCE_STORAGE_KEY` | `'layout-appearance'` |
451
- | `LAYOUT_TYPE_STORAGE_KEY` | `'layout-type'` |
452
- | `LAYOUT_WIDTH_STORAGE_KEY` | `'layout-width'` |
444
+ | Symbol | Nilai |
445
+ | ------------------------------- | ------------------------------------------------------------------------------------------ |
446
+ | `LAYOUT_TYPES` | `['vertical', 'horizontal', 'empty', 'fluid']` |
447
+ | `LAYOUT_SURFACES` | `['flat', 'grid', 'grid-line', 'honeycomb', 'matrix', 'circuit', 'line-vertical', 'line-horizontal']` |
448
+ | `LAYOUT_APPEARANCES` | `['flat', 'border-rail']` |
449
+ | `LAYOUT_WIDTHS` | `['full', 'wide', 'container', 'fluid']` |
450
+ | `LAYOUT_DEFAULT_SURFACE` | `'flat'` |
451
+ | `LAYOUT_DEFAULT_TYPE` | `'vertical'` |
452
+ | `LAYOUT_DEFAULT_APPEARANCE` | `'flat'` |
453
+ | `LAYOUT_DEFAULT_WIDTH` | `'full'` |
454
+ | `LAYOUT_SURFACE_STORAGE_KEY` | `'layout-surface'` |
455
+ | `LAYOUT_APPEARANCE_STORAGE_KEY` | `'layout-appearance'` |
456
+ | `LAYOUT_TYPE_STORAGE_KEY` | `'layout-type'` |
457
+ | `LAYOUT_WIDTH_STORAGE_KEY` | `'layout-width'` |
453
458
 
454
459
  ### Guards
455
460
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ojiepermana/angular-theme",
3
- "version": "22.0.36",
3
+ "version": "22.0.43",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/edsis/angular.git"
@@ -13,8 +13,8 @@
13
13
  "@angular/common": ">=22.0.0",
14
14
  "@angular/core": ">=22.0.0",
15
15
  "@angular/router": ">=22.0.0",
16
- "@ojiepermana/angular-navigation": "^22.0.36",
17
- "@ojiepermana/angular-component": "^22.0.36",
16
+ "@ojiepermana/angular-navigation": "^22.0.43",
17
+ "@ojiepermana/angular-component": "^22.0.43",
18
18
  "rxjs": ">=7.8.0"
19
19
  },
20
20
  "dependencies": {
package/page/README.md CHANGED
@@ -21,22 +21,48 @@ Dokumentasi ini mengikuti API publik terbaru dari `@ojiepermana/angular-theme/pa
21
21
 
22
22
  ### `Page`
23
23
 
24
- | Input | Type | Default | Keterangan |
25
- | ----------- | ----------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------- |
26
- | `variant` | `'stacked' \| 'side'` | `'stacked'` | Menentukan struktur utama page shell. |
27
- | `scroll` | `'content' \| 'Page'` | `'content'` | Menentukan apakah body slot scroll sendiri atau seluruh root `Page` scroll bersama. |
28
- | `height` | `'auto' \| 'fix'` | `'auto'` | Mengontrol tinggi `PageHeader` dan `PageFooter`. |
29
- | `position` | `'left' \| 'right'` | `'left'` | Posisi default untuk `PageSide` jika side tidak memberi override sendiri. |
30
- | `sideMode` | `'sticky' \| 'drawer' \| 'overlay'` | `'sticky'` | Mode default untuk `PageSide` jika side tidak memberi override sendiri. |
31
- | `sideWidth` | `string` | `'16rem'` | Lebar default side rail; dipakai juga untuk drawer dan overlay. |
32
- | `sideOpen` | `boolean \| null` | `null` | Saat `null`, state side tidak dikontrol dari parent. Saat `true/false`, komponen masuk controlled mode. |
33
- | `class` | `string` | `''` | Class tambahan pada host `Page`. |
24
+ | Input | Type | Default | Keterangan |
25
+ | ------------ | ----------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------- |
26
+ | `variant` | `'stacked' \| 'side'` | `'stacked'` | Menentukan struktur utama page shell. |
27
+ | `scroll` | `'content' \| 'page'` | `'content'` | Menentukan apakah body slot scroll sendiri atau seluruh root `Page` scroll bersama. |
28
+ | `height` | `'auto' \| 'fix'` | `'auto'` | Mengontrol tinggi `PageHeader` dan `PageFooter`. |
29
+ | `appearance` | `'flat' \| 'border-rail'` | `'flat'` | Appearance visual yang dibagi dengan layout/navigation; menyeragamkan border header/footer (`border-rail` = 1.5px). |
30
+ | `position` | `'left' \| 'right'` | `'left'` | Posisi default untuk `PageSide` jika side tidak memberi override sendiri. |
31
+ | `sideMode` | `'sticky' \| 'drawer' \| 'overlay'` | `'sticky'` | Mode default untuk `PageSide` jika side tidak memberi override sendiri. |
32
+ | `sideWidth` | `string` | `'16rem'` | Lebar default side rail; dipakai juga untuk drawer dan overlay. |
33
+ | `sideOpen` | `boolean \| null` | `null` | Saat `null`, state side tidak dikontrol dari parent. Saat `true/false`, komponen masuk controlled mode. |
34
+ | `class` | `string` | `''` | Class tambahan pada host `Page`. |
34
35
 
35
36
  | Output | Payload | Keterangan |
36
37
  | ---------------- | --------- | -------------------------------------------------------------------------------------------- |
37
38
  | `sideOpenChange` | `boolean` | Emit setiap ada permintaan buka/tutup side, baik dari toggle, backdrop, maupun tombol `Esc`. |
38
39
 
39
- `Page` juga menambahkan atribut host `data-page-variant`, `data-page-height`, `data-page-scroll`, `data-page-position`, `data-page-side-mode`, dan `data-page-side-open` untuk styling atau inspeksi.
40
+ `Page` juga menambahkan atribut host `data-page-variant`, `data-page-height`, `data-page-scroll`, `data-page-appearance`, `data-page-position`, `data-page-side-mode`, dan `data-page-side-open` untuk styling atau inspeksi.
41
+
42
+ ### Apps launcher (layout `empty`)
43
+
44
+ Saat `Page` dirender di dalam layout `empty`, sebuah tombol icon-only tanpa border (ikon `apps`) muncul **otomatis** di pojok kanan-atas dan membuka **main navigation** (`id="main"`) sebagai flyout. Penempatannya menyesuaikan struktur `Page`:
45
+
46
+ - Ada `PageHeader` → tombol menyatu di kanan header.
47
+ - Tanpa `PageHeader` (mis. hanya `PageDashboard`) → tombol mengambang di atas konten.
48
+
49
+ Tombol hanya muncul ketika layout aktif `empty` dan nav target punya data. Di layout lain (`vertical`/`horizontal`/`fluid`) tombol tidak dirender karena navigasi utama sudah terlihat.
50
+
51
+ | Input | Type | Default | Keterangan |
52
+ | -------------- | --------- | ------------------- | ------------------------------------------------------------ |
53
+ | `appsLauncher` | `boolean` | `true` | Aktif/nonaktifkan apps-launcher pada layout `empty`. |
54
+ | `appsNavId` | `string` | `'main'` | Id navigasi yang disurface oleh apps-launcher. |
55
+ | `appsIcon` | `string` | `'apps'` | Nama ikon Material untuk tombol. |
56
+ | `appsLabel` | `string` | `'Open navigation'` | Label/aria-label tombol icon-only (untuk aksesibilitas AXE). |
57
+
58
+ ```html
59
+ <!-- Dashboard tanpa header: tombol apps mengambang di kanan-atas saat layout empty. -->
60
+ <Page variant="stacked">
61
+ <PageDashboard>
62
+ <p>Konten dashboard.</p>
63
+ </PageDashboard>
64
+ </Page>
65
+ ```
40
66
 
41
67
  ## Slot API
42
68
 
@@ -63,7 +89,7 @@ Dokumentasi ini mengikuti API publik terbaru dari `@ojiepermana/angular-theme/pa
63
89
 
64
90
  | Input / Output | Type | Default | Keterangan |
65
91
  | -------------- | --------- | -------------------- | ----------------------------------------------------------- |
66
- | `ariaLabel` | `string` | `'Toggle Page side'` | Label aksesibilitas untuk tombol toggle. |
92
+ | `ariaLabel` | `string` | `'Toggle page side'` | Label aksesibilitas untuk tombol toggle. |
67
93
  | `class` | `string` | `''` | Class tambahan pada host toggle. |
68
94
  | `toggled` | `boolean` | - | Emit nilai open state terbaru setelah tombol toggle diklik. |
69
95
 
@@ -75,7 +101,7 @@ Dokumentasi ini mengikuti API publik terbaru dari `@ojiepermana/angular-theme/pa
75
101
  ## Scroll Modes
76
102
 
77
103
  - `content` (default): `PageContent` atau `PageDashboard` menjadi area scroll. Pada variant `side`, `PageSide` dan body slot yang dipakai sama-sama stretch ke parent dan masing-masing punya scroll sendiri.
78
- - `Page`: seluruh isi `Page` menjadi area scroll. Header, content, footer, dan pada variant `side` juga `PageSide` akan mengikuti tinggi content induk tanpa scroll internal.
104
+ - `page`: seluruh isi `Page` menjadi area scroll. Header, content, footer, dan pada variant `side` juga `PageSide` akan mengikuti tinggi content induk tanpa scroll internal.
79
105
 
80
106
  ## Height Modes
81
107
 
@@ -86,7 +112,7 @@ Dalam mode `fix`, pastikan isi header/footer memang dirancang sebagai rail ringk
86
112
 
87
113
  ## Behavior Summary
88
114
 
89
- | Variant | `scroll="content"` | `scroll="Page"` |
115
+ | Variant | `scroll="content"` | `scroll="page"` |
90
116
  | --------- | ------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
91
117
  | `stacked` | `PageContent` atau `PageDashboard` stretch ke tinggi yang tersedia dan menjadi area scroll | seluruh `Page` scroll bersama; header, content, footer ikut satu kanvas |
92
118
  | `side` | `PageSide` dan body slot (`PageContent`/`PageDashboard`) sama-sama stretch ke parent dan punya scroll sendiri | `PageSide` dan body slot mengikuti tinggi content induk tanpa scroll internal |
@@ -126,6 +152,18 @@ Saat memakai `height="fix"`, slot berikut tidak boleh melebihi tinggi tetap yang
126
152
 
127
153
  ### Stacked dashboard surface
128
154
 
155
+ `PageDashboard` berdiri sendiri — tidak memerlukan `PageHeader` maupun `PageFooter`. Gunakan slot ini ketika area utama secara semantik adalah dashboard atau board analitik.
156
+
157
+ ```html
158
+ <Page variant="stacked">
159
+ <PageDashboard>
160
+ <p>Gunakan slot ini ketika area utama secara semantik adalah dashboard atau board analitik.</p>
161
+ </PageDashboard>
162
+ </Page>
163
+ ```
164
+
165
+ `PageHeader` dan `PageFooter` tetap opsional bila memang dibutuhkan:
166
+
129
167
  ```html
130
168
  <Page variant="stacked">
131
169
  <PageHeader>
@@ -133,7 +171,7 @@ Saat memakai `height="fix"`, slot berikut tidak boleh melebihi tinggi tetap yang
133
171
  </PageHeader>
134
172
 
135
173
  <PageDashboard>
136
- <p>Gunakan slot ini ketika area utama secara semantik adalah dashboard atau board analitik.</p>
174
+ <p>Dashboard dengan header dan footer eksplisit.</p>
137
175
  </PageDashboard>
138
176
 
139
177
  <PageFooter>
package/styles/README.md CHANGED
@@ -12,7 +12,9 @@ export const appConfig = {
12
12
  mode: 'light',
13
13
  color: 'base', // accent: base, red…rose, brand
14
14
  neutral: 'base', // gray family: base, slate, gray, zinc, …
15
- brand: '221 83% 53%', // warna brand consumer (HSL triplet)
15
+ radius: 'md', // corner radius: none, sm, md, lg, xl, full
16
+ space: 'normal', // spacing density: compact, normal, relaxed, spacious
17
+ brand: '221 83% 53%', // warna brand consumer (HSL triplet, atau { color, foreground })
16
18
  }),
17
19
  ],
18
20
  };
@@ -23,10 +25,16 @@ export const appConfig = {
23
25
  - `mode` mem-bootstrap `ThemeModeService` dan default mode yang disimpan di storage.
24
26
  - `color` mem-bootstrap `ThemeColorService`; accent palette awal (`<html theme-color>`).
25
27
  - `neutral` keluarga neutral awal (`<html theme-neutral>`); berkomposisi dengan accent.
28
+ - `radius` mem-bootstrap `ThemeRadiusService`; preset corner radius awal (`<html theme-radius>`).
29
+ Menggerakkan knob `--radius-base` sehingga seluruh skala `--radius-*` dan utility `rounded-*` ikut.
30
+ - `space` mem-bootstrap `ThemeSpaceService`; preset spacing density awal (`<html theme-space>`).
31
+ Menggerakkan knob `--spacing-base` sehingga setiap utility `p-*`/`m-*`/`gap-*`/`w-*`/`h-*` ikut.
26
32
  - `brand` mem-bootstrap `ThemeBrandService`; set `--brand` / utility `bg-brand` dan preset
27
- accent `theme-color='brand'`. Bisa diubah runtime via `setBrand()`, persist di storage.
33
+ accent `theme-color='brand'`. Menerima string HSL triplet atau `{ color, foreground }`.
34
+ Bisa diubah runtime via `setBrand()`, persist di storage.
28
35
  - `icons.materialSymbols` mengontrol preload stylesheet Material Symbols pada bootstrap aplikasi.
29
- - Pilihan yang dipersist (`theme-color` / `theme-neutral` / `theme-brand`) menang atas default.
36
+ - Pilihan yang dipersist (`theme-color` / `theme-neutral` / `theme-radius` / `theme-space` /
37
+ `theme-brand`) menang atas default.
30
38
 
31
39
  ## Default Behavior
32
40
 
@@ -126,4 +126,216 @@
126
126
  line-height: var(--nav-badge-line-height, var(--text-sm--line-height));
127
127
  font-weight: var(--nav-badge-weight, 500);
128
128
  }
129
+
130
+ /* Layout `honeycomb` surface — a TRUE tessellated flat-top hexagon grid drawn
131
+ * as a token-colored mask layer, so it recolors with the active theme/neutral
132
+ * (light + dark) like the other `<Layout surface>` patterns. Adjacent hexagons
133
+ * SHARE a single edge (no gaps), and `vector-effect: non-scaling-stroke` pins
134
+ * every line to a fixed 1px regardless of cell size. The SVG tile is exactly
135
+ * one lattice period (width 3s × height s√3, s=24px) so `mask-repeat` tiles it
136
+ * seamlessly; the path is solid-stroke and faintness is owned by
137
+ * --layout-grid-line-opacity (the dense `grid` tier, since honeycomb is
138
+ * multi-axis). Tune cell size via --layout-hex-size — keep the √3 (≈1.732)
139
+ * width:height ratio or the hexagons stretch. Edit the pattern here, not
140
+ * per-theme. */
141
+ .layout-surface-honeycomb {
142
+ position: relative;
143
+ --layout-hex-size: 43.2px 24.941px;
144
+ --layout-hex-mask: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='72' height='41.569' viewBox='0 0 72 41.569'><path d='M-12 -62.354L-24 -83.138L-48 -83.138L-60 -62.354L-48 -41.569L-24 -41.569ZM-12 -20.785L-24 -41.569L-48 -41.569L-60 -20.785L-48 0L-24 0ZM-12 20.785L-24 0L-48 0L-60 20.785L-48 41.569L-24 41.569ZM-12 62.354L-24 41.569L-48 41.569L-60 62.354L-48 83.138L-24 83.138ZM-12 103.923L-24 83.138L-48 83.138L-60 103.923L-48 124.708L-24 124.708ZM-12 145.492L-24 124.708L-48 124.708L-60 145.492L-48 166.277L-24 166.277ZM24 -83.138L12 -103.923L-12 -103.923L-24 -83.138L-12 -62.354L12 -62.354ZM24 -41.569L12 -62.354L-12 -62.354L-24 -41.569L-12 -20.785L12 -20.785ZM24 0L12 -20.785L-12 -20.785L-24 0L-12 20.785L12 20.785ZM24 41.569L12 20.785L-12 20.785L-24 41.569L-12 62.354L12 62.354ZM24 83.138L12 62.354L-12 62.354L-24 83.138L-12 103.923L12 103.923ZM24 124.708L12 103.923L-12 103.923L-24 124.708L-12 145.492L12 145.492ZM60 -62.354L48 -83.138L24 -83.138L12 -62.354L24 -41.569L48 -41.569ZM60 -20.785L48 -41.569L24 -41.569L12 -20.785L24 0L48 0ZM60 20.785L48 0L24 0L12 20.785L24 41.569L48 41.569ZM60 62.354L48 41.569L24 41.569L12 62.354L24 83.138L48 83.138ZM60 103.923L48 83.138L24 83.138L12 103.923L24 124.708L48 124.708ZM60 145.492L48 124.708L24 124.708L12 145.492L24 166.277L48 166.277ZM96 -83.138L84 -103.923L60 -103.923L48 -83.138L60 -62.354L84 -62.354ZM96 -41.569L84 -62.354L60 -62.354L48 -41.569L60 -20.785L84 -20.785ZM96 0L84 -20.785L60 -20.785L48 0L60 20.785L84 20.785ZM96 41.569L84 20.785L60 20.785L48 41.569L60 62.354L84 62.354ZM96 83.138L84 62.354L60 62.354L48 83.138L60 103.923L84 103.923ZM96 124.708L84 103.923L60 103.923L48 124.708L60 145.492L84 145.492ZM132 -62.354L120 -83.138L96 -83.138L84 -62.354L96 -41.569L120 -41.569ZM132 -20.785L120 -41.569L96 -41.569L84 -20.785L96 0L120 0ZM132 20.785L120 0L96 0L84 20.785L96 41.569L120 41.569ZM132 62.354L120 41.569L96 41.569L84 62.354L96 83.138L120 83.138ZM132 103.923L120 83.138L96 83.138L84 103.923L96 124.708L120 124.708ZM132 145.492L120 124.708L96 124.708L84 145.492L96 166.277L120 166.277Z' fill='none' stroke='%23000' stroke-width='1' vector-effect='non-scaling-stroke'/></svg>");
145
+ }
146
+
147
+ .layout-surface-honeycomb::before {
148
+ content: '';
149
+ position: absolute;
150
+ inset: 0;
151
+ z-index: 0;
152
+ pointer-events: none;
153
+ background-color: hsl(var(--layout-grid-line) / var(--layout-grid-line-opacity));
154
+ -webkit-mask-image: var(--layout-hex-mask);
155
+ mask-image: var(--layout-hex-mask);
156
+ -webkit-mask-repeat: repeat;
157
+ mask-repeat: repeat;
158
+ -webkit-mask-position: center;
159
+ mask-position: center;
160
+ -webkit-mask-size: var(--layout-hex-size);
161
+ mask-size: var(--layout-hex-size);
162
+ }
163
+
164
+ /* Layout `matrix` surface — a scattered field of binary digits (0/1) at
165
+ * varied sizes with per-glyph opacity baked into the SVG, painted as a mask
166
+ * layer so it recolors with the active theme like the other `<Layout surface>`
167
+ * patterns. Unlike the neutral grid surfaces it is tinted with the brand
168
+ * --primary accent (--layout-matrix-opacity is the global faintness knob, toned
169
+ * down for dark mode in base/tokens.css), giving a soft "digital" signature
170
+ * behind content. The SVG tile is one square period (--layout-matrix-size) and
171
+ * `mask-repeat` tiles it seamlessly; every glyph sits inside the tile so the
172
+ * seam never cuts a digit. Edit the field here, not per-theme. */
173
+ .layout-surface-matrix {
174
+ position: relative;
175
+ --layout-matrix-mask: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='240' height='240' viewBox='0 0 240 240'><g font-family='monospace' fill='black' text-anchor='middle' dominant-baseline='middle'><text x='14' y='12' font-size='13' fill-opacity='.35'>0</text><text x='43' y='11' font-size='12' fill-opacity='.85'>0</text><text x='59' y='11' font-size='11' fill-opacity='.45'>0</text><text x='80' y='15' font-size='12' fill-opacity='.45'>0</text><text x='110' y='17' font-size='8' fill-opacity='.3'>1</text><text x='137' y='11' font-size='9' fill-opacity='.6'>1</text><text x='150' y='11' font-size='10' fill-opacity='.35'>0</text><text x='179' y='18' font-size='10' fill-opacity='.85'>0</text><text x='203' y='17' font-size='12' fill-opacity='.7'>0</text><text x='226' y='15' font-size='10' fill-opacity='1'>1</text><text x='21' y='37' font-size='10' fill-opacity='.45'>0</text><text x='34' y='37' font-size='11' fill-opacity='.7'>1</text><text x='60' y='45' font-size='9' fill-opacity='.4'>1</text><text x='88' y='35' font-size='11' fill-opacity='.3'>1</text><text x='114' y='34' font-size='12' fill-opacity='.55'>1</text><text x='134' y='40' font-size='12' fill-opacity='.35'>1</text><text x='159' y='44' font-size='11' fill-opacity='.3'>0</text><text x='181' y='37' font-size='12' fill-opacity='.5'>1</text><text x='203' y='44' font-size='10' fill-opacity='.7'>0</text><text x='222' y='40' font-size='11' fill-opacity='.45'>0</text><text x='19' y='58' font-size='10' fill-opacity='.6'>1</text><text x='44' y='62' font-size='9' fill-opacity='.6'>1</text><text x='63' y='67' font-size='11' fill-opacity='.6'>1</text><text x='91' y='64' font-size='11' fill-opacity='.4'>0</text><text x='103' y='58' font-size='13' fill-opacity='.3'>0</text><text x='131' y='63' font-size='10' fill-opacity='.3'>1</text><text x='150' y='63' font-size='12' fill-opacity='.4'>1</text><text x='180' y='62' font-size='12' fill-opacity='.7'>0</text><text x='206' y='66' font-size='13' fill-opacity='.6'>1</text><text x='223' y='57' font-size='13' fill-opacity='.3'>1</text><text x='12' y='91' font-size='11' fill-opacity='.35'>0</text><text x='37' y='80' font-size='8' fill-opacity='.85'>0</text><text x='57' y='84' font-size='8' fill-opacity='.45'>0</text><text x='87' y='81' font-size='10' fill-opacity='1'>1</text><text x='107' y='81' font-size='11' fill-opacity='.7'>1</text><text x='131' y='80' font-size='9' fill-opacity='.5'>1</text><text x='154' y='88' font-size='12' fill-opacity='.45'>0</text><text x='183' y='86' font-size='9' fill-opacity='.85'>0</text><text x='198' y='87' font-size='9' fill-opacity='.85'>1</text><text x='222' y='81' font-size='15' fill-opacity='.85'>0</text><text x='16' y='108' font-size='13' fill-opacity='1'>0</text><text x='43' y='114' font-size='10' fill-opacity='.6'>0</text><text x='65' y='105' font-size='12' fill-opacity='.55'>1</text><text x='88' y='114' font-size='15' fill-opacity='.7'>1</text><text x='106' y='111' font-size='10' fill-opacity='.55'>1</text><text x='137' y='107' font-size='10' fill-opacity='.45'>0</text><text x='154' y='106' font-size='11' fill-opacity='.7'>0</text><text x='183' y='107' font-size='13' fill-opacity='.35'>0</text><text x='206' y='112' font-size='15' fill-opacity='.7'>0</text><text x='229' y='108' font-size='13' fill-opacity='.35'>1</text><text x='20' y='137' font-size='11' fill-opacity='.6'>1</text><text x='42' y='127' font-size='9' fill-opacity='.4'>0</text><text x='57' y='133' font-size='11' fill-opacity='1'>0</text><text x='89' y='137' font-size='13' fill-opacity='.4'>1</text><text x='109' y='127' font-size='8' fill-opacity='.85'>0</text><text x='135' y='127' font-size='10' fill-opacity='.3'>0</text><text x='152' y='129' font-size='10' fill-opacity='.5'>1</text><text x='178' y='136' font-size='8' fill-opacity='.7'>1</text><text x='203' y='135' font-size='12' fill-opacity='.85'>1</text><text x='220' y='127' font-size='12' fill-opacity='.7'>0</text><text x='19' y='156' font-size='15' fill-opacity='.4'>0</text><text x='35' y='156' font-size='9' fill-opacity='.55'>0</text><text x='64' y='155' font-size='11' fill-opacity='.85'>0</text><text x='80' y='151' font-size='8' fill-opacity='.85'>0</text><text x='108' y='149' font-size='9' fill-opacity='.55'>1</text><text x='133' y='155' font-size='12' fill-opacity='.5'>0</text><text x='154' y='155' font-size='11' fill-opacity='.85'>0</text><text x='182' y='160' font-size='10' fill-opacity='.7'>0</text><text x='197' y='150' font-size='11' fill-opacity='.35'>1</text><text x='226' y='154' font-size='10' fill-opacity='.35'>1</text><text x='21' y='174' font-size='14' fill-opacity='.4'>1</text><text x='36' y='173' font-size='11' fill-opacity='.35'>0</text><text x='61' y='178' font-size='13' fill-opacity='.4'>0</text><text x='88' y='184' font-size='11' fill-opacity='.6'>1</text><text x='105' y='176' font-size='14' fill-opacity='.3'>1</text><text x='130' y='177' font-size='14' fill-opacity='.6'>0</text><text x='153' y='179' font-size='12' fill-opacity='.35'>0</text><text x='184' y='181' font-size='9' fill-opacity='.5'>0</text><text x='198' y='183' font-size='9' fill-opacity='.4'>1</text><text x='228' y='182' font-size='13' fill-opacity='.6'>1</text><text x='12' y='206' font-size='12' fill-opacity='.55'>1</text><text x='34' y='196' font-size='14' fill-opacity='.6'>0</text><text x='67' y='198' font-size='8' fill-opacity='.5'>0</text><text x='80' y='205' font-size='9' fill-opacity='.35'>1</text><text x='108' y='199' font-size='12' fill-opacity='.5'>1</text><text x='133' y='195' font-size='14' fill-opacity='.35'>0</text><text x='160' y='198' font-size='9' fill-opacity='.5'>0</text><text x='179' y='201' font-size='10' fill-opacity='.7'>1</text><text x='201' y='197' font-size='10' fill-opacity='.5'>0</text><text x='218' y='195' font-size='12' fill-opacity='.85'>0</text><text x='16' y='229' font-size='9' fill-opacity='.7'>1</text><text x='40' y='229' font-size='12' fill-opacity='.45'>1</text><text x='68' y='222' font-size='14' fill-opacity='.6'>0</text><text x='91' y='230' font-size='9' fill-opacity='.35'>0</text><text x='110' y='229' font-size='11' fill-opacity='.3'>0</text><text x='127' y='228' font-size='12' fill-opacity='1'>1</text><text x='152' y='222' font-size='11' fill-opacity='.4'>0</text><text x='175' y='218' font-size='10' fill-opacity='.85'>1</text><text x='199' y='218' font-size='10' fill-opacity='.55'>0</text><text x='220' y='222' font-size='9' fill-opacity='.5'>1</text></g></svg>");
176
+ /* Glow layer mask — ~10% of the digits (10 of 100), placed only in the tile's
177
+ * EDGE bands (never the center). Each is drawn twice through a `feGaussianBlur`
178
+ * filter (soft bloom halo) plus once sharp (lit core); painted brighter than the
179
+ * field via --layout-matrix-glow-opacity on `::after`. Static — no animation. */
180
+ --layout-matrix-glow-mask: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='240' height='240' viewBox='0 0 240 240'><defs><filter id='g' x='-80%' y='-80%' width='260%' height='260%'><feGaussianBlur stdDeviation='2.2'/></filter></defs><g font-family='monospace' fill='black' text-anchor='middle' dominant-baseline='middle'><g filter='url(%23g)'><text x='43' y='11' font-size='12' fill-opacity='1'>0</text><text x='203' y='17' font-size='12' fill-opacity='1'>0</text><text x='12' y='91' font-size='11' fill-opacity='1'>0</text><text x='12' y='206' font-size='12' fill-opacity='1'>1</text><text x='226' y='154' font-size='10' fill-opacity='1'>1</text><text x='229' y='108' font-size='13' fill-opacity='1'>1</text><text x='40' y='229' font-size='12' fill-opacity='1'>1</text><text x='175' y='218' font-size='10' fill-opacity='1'>1</text><text x='179' y='18' font-size='10' fill-opacity='1'>0</text><text x='16' y='108' font-size='13' fill-opacity='1'>0</text></g><g filter='url(%23g)'><text x='43' y='11' font-size='12' fill-opacity='1'>0</text><text x='203' y='17' font-size='12' fill-opacity='1'>0</text><text x='12' y='91' font-size='11' fill-opacity='1'>0</text><text x='12' y='206' font-size='12' fill-opacity='1'>1</text><text x='226' y='154' font-size='10' fill-opacity='1'>1</text><text x='229' y='108' font-size='13' fill-opacity='1'>1</text><text x='40' y='229' font-size='12' fill-opacity='1'>1</text><text x='175' y='218' font-size='10' fill-opacity='1'>1</text><text x='179' y='18' font-size='10' fill-opacity='1'>0</text><text x='16' y='108' font-size='13' fill-opacity='1'>0</text></g><g><text x='43' y='11' font-size='12' fill-opacity='1'>0</text><text x='203' y='17' font-size='12' fill-opacity='1'>0</text><text x='12' y='91' font-size='11' fill-opacity='1'>0</text><text x='12' y='206' font-size='12' fill-opacity='1'>1</text><text x='226' y='154' font-size='10' fill-opacity='1'>1</text><text x='229' y='108' font-size='13' fill-opacity='1'>1</text><text x='40' y='229' font-size='12' fill-opacity='1'>1</text><text x='175' y='218' font-size='10' fill-opacity='1'>1</text><text x='179' y='18' font-size='10' fill-opacity='1'>0</text><text x='16' y='108' font-size='13' fill-opacity='1'>0</text></g></g></svg>");
181
+ }
182
+
183
+ .layout-surface-matrix::before {
184
+ content: '';
185
+ position: absolute;
186
+ inset: 0;
187
+ z-index: 0;
188
+ pointer-events: none;
189
+ background-color: hsl(var(--primary) / var(--layout-matrix-opacity));
190
+ -webkit-mask-image: var(--layout-matrix-mask);
191
+ mask-image: var(--layout-matrix-mask);
192
+ -webkit-mask-repeat: repeat;
193
+ mask-repeat: repeat;
194
+ -webkit-mask-position: center;
195
+ mask-position: center;
196
+ -webkit-mask-size: var(--layout-matrix-size);
197
+ mask-size: var(--layout-matrix-size);
198
+ }
199
+
200
+ /* Sparse edge-glow accents: the glow glyph mask (repeating) intersected with a
201
+ * radial edge-vignette so the lit digits appear only toward the layout edges
202
+ * and fade out behind centered content (`hindari di tengah`). `intersect`
203
+ * (std) / `source-in` (-webkit-) keep both masks ANDed. */
204
+ .layout-surface-matrix::after {
205
+ content: '';
206
+ position: absolute;
207
+ inset: 0;
208
+ z-index: 0;
209
+ pointer-events: none;
210
+ background-color: hsl(var(--primary) / var(--layout-matrix-glow-opacity));
211
+ -webkit-mask-image: var(--layout-matrix-glow-mask),
212
+ radial-gradient(ellipse 78% 78% at center, transparent 36%, #000 80%);
213
+ mask-image: var(--layout-matrix-glow-mask),
214
+ radial-gradient(ellipse 78% 78% at center, transparent 36%, #000 80%);
215
+ -webkit-mask-repeat: repeat, no-repeat;
216
+ mask-repeat: repeat, no-repeat;
217
+ -webkit-mask-position: center, center;
218
+ mask-position: center, center;
219
+ -webkit-mask-size: var(--layout-matrix-size), 100% 100%;
220
+ mask-size: var(--layout-matrix-size), 100% 100%;
221
+ -webkit-mask-composite: source-in;
222
+ mask-composite: intersect;
223
+ }
224
+
225
+ /* Layout `grid-line` surface — a dark mosaic of 2rem square cells; a richer
226
+ * cousin of `grid`. A faint uniform grid with scattered BRIGHT highlight
227
+ * squares + L-corners and a few subtly-lit cells, all painted as ONE
228
+ * token-colored mask layer (same mechanism as `.layout-surface-honeycomb` /
229
+ * `.layout-surface-matrix`). EVERYTHING lives in the single mask — base
230
+ * grid, glows, and cell fills — so the layers stay pixel-aligned. The glow
231
+ * FOLLOWS THE ACCENT: the ::before paints with the accent hue+saturation (from
232
+ * --accent-foreground) but pins lightness to --layout-grid-highlight-l, so the
233
+ * surface tracks the active theme-color while its brightness stays put (in the
234
+ * base/neutral theme the accent is achromatic → the same grayscale as before).
235
+ * --layout-grid-highlight-opacity is the global faintness knob. Every fill /
236
+ * stroke is white (%23fff) and serves only as an ALPHA mask: low alpha = faint
237
+ * grid/cell, high alpha = bright glow. `vector-effect: non-scaling-stroke`
238
+ * pins every line to a fixed 1px regardless of cell size.
239
+ *
240
+ * The tile is 6x6 cells (viewBox 0 0 192 192 = 32 units/cell) so mask-size is
241
+ * 9.96rem (one cell ≈ 1.66rem — 17% smaller than the 2rem `grid` family size;
242
+ * tune via --layout-grid-line-size). SEAMLESS: the grid is drawn
243
+ * at 0..192 on BOTH edges. A 1px non-scaling stroke centered on a tile edge has
244
+ * its outer half clipped by the SVG viewBox, so under `mask-repeat: repeat` each
245
+ * seam gets one half-stroke from the tile on each side ([191.5,192] + [192,192.5])
246
+ * which combine into a single full 1px line — an even grid with no doubling and
247
+ * no thinned seam. Every highlight/fill stays in the interior (>=32, <=160) so
248
+ * no glow is ever clipped at a tile edge. Edit the pattern here, not per-theme. */
249
+ .layout-surface-grid-line {
250
+ position: relative;
251
+ --layout-grid-line-size: 9.96rem 9.96rem;
252
+ --layout-grid-line-mask: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='192' height='192' viewBox='0 0 192 192'><path d='M0 0V192M32 0V192M64 0V192M96 0V192M128 0V192M160 0V192M192 0V192M0 0H192M0 32H192M0 64H192M0 96H192M0 128H192M0 160H192M0 192H192' fill='none' stroke='%23fff' stroke-width='1' stroke-opacity='0.25' vector-effect='non-scaling-stroke'/><g fill='%23fff'><rect x='66' y='34' width='28' height='28' fill-opacity='0.1'/><rect x='130' y='34' width='28' height='28' fill-opacity='0.06'/><rect x='34' y='66' width='28' height='28' fill-opacity='0.12'/><rect x='98' y='66' width='28' height='28' fill-opacity='0.07'/><rect x='66' y='98' width='28' height='28' fill-opacity='0.09'/><rect x='130' y='98' width='28' height='28' fill-opacity='0.05'/><rect x='34' y='130' width='28' height='28' fill-opacity='0.11'/><rect x='98' y='130' width='28' height='28' fill-opacity='0.06'/></g><g fill='none' stroke='%23fff' stroke-width='1' vector-effect='non-scaling-stroke'><rect x='32' y='32' width='32' height='32' stroke-opacity='0.85'/><rect x='128' y='64' width='32' height='32' stroke-opacity='0.8'/><rect x='32' y='96' width='32' height='32' stroke-opacity='0.9'/><rect x='128' y='128' width='32' height='32' stroke-opacity='0.82'/><path d='M128 32L96 32L96 64' stroke-opacity='0.7'/><path d='M64 96L96 96L96 64' stroke-opacity='0.75'/><path d='M96 96L128 96L128 128' stroke-opacity='0.72'/><path d='M64 128L64 160L96 160' stroke-opacity='0.68'/></g></svg>");
253
+ }
254
+
255
+ .layout-surface-grid-line::before {
256
+ content: '';
257
+ position: absolute;
258
+ inset: 0;
259
+ z-index: 0;
260
+ pointer-events: none;
261
+ /* Accent-tinted glow: take the accent hue+saturation (from --accent-foreground)
262
+ * but PIN the lightness to --layout-grid-highlight-l, so the surface follows
263
+ * the active theme-color while its brightness stays exactly as before. In the
264
+ * base/neutral theme the accent is achromatic, so it resolves to the same
265
+ * grayscale. The first declaration is a grayscale fallback for browsers without
266
+ * CSS relative color; the second tints it where supported. */
267
+ background-color: hsl(0 0% var(--layout-grid-highlight-l) / var(--layout-grid-highlight-opacity));
268
+ background-color: hsl(
269
+ from hsl(var(--accent-foreground)) h s var(--layout-grid-highlight-l) /
270
+ var(--layout-grid-highlight-opacity)
271
+ );
272
+ -webkit-mask-image: var(--layout-grid-line-mask);
273
+ mask-image: var(--layout-grid-line-mask);
274
+ -webkit-mask-repeat: repeat;
275
+ mask-repeat: repeat;
276
+ -webkit-mask-position: center;
277
+ mask-position: center;
278
+ -webkit-mask-size: var(--layout-grid-line-size);
279
+ mask-size: var(--layout-grid-line-size);
280
+ }
281
+
282
+ /* Layout `circuit` surface — a tileable PCB / circuit-board texture: routed
283
+ * copper traces with 45° chamfered bends, via nodes, and concentric ring pads,
284
+ * echoing a printed circuit board. Built as the same kind of token-colored mask
285
+ * layer as `.layout-surface-honeycomb` / `.layout-surface-matrix`, but with TWO
286
+ * stacked layers because a mask only carries ALPHA (one color per layer):
287
+ * ::before — the neutral copper TRACES, painted in --layout-grid-line (the
288
+ * same neutral the grid family uses, so the board recolors per
289
+ * theme + dark mode). Faintness = --layout-circuit-opacity × the
290
+ * per-trace alpha baked into the SVG (ghost 0.22 · trace 0.55-0.6 ·
291
+ * trunk 0.9), giving the copper-weight variation of a real board.
292
+ * ::after — the via NODES + concentric pads, painted in the brand --primary
293
+ * accent (like `matrix`) so the connection points GLOW with the
294
+ * theme while the traces stay grayscale. Faintness =
295
+ * --layout-circuit-node-opacity (toned down for dark mode in
296
+ * base/tokens.css, since --primary inverts to near-white and blooms).
297
+ * Both layers share ONE tile geometry (--layout-circuit-size) and `mask-position:
298
+ * center`, so traces and nodes stay pixel-aligned. `vector-effect:
299
+ * non-scaling-stroke` pins every line to its authored px width at any scale.
300
+ *
301
+ * SEAMLESS BY CONSTRUCTION (240x240 tile): every horizontal bus enters at x=0
302
+ * and exits at x=240 at the SAME y (wraps left/right) and every vertical bus
303
+ * enters at y=0 / exits at y=240 at the SAME x (wraps top/bottom); the long
304
+ * buses bridge each seam so the board reads continuous. All interior branches,
305
+ * vias, and rings stay inside an 18px edge keep-out, so tiling never clips a
306
+ * node. Edit the board in scratchpad/circuit-gen.mjs (the parametric generator
307
+ * with a built-in seam check), not by hand. */
308
+ .layout-surface-circuit {
309
+ position: relative;
310
+ --layout-circuit-size: 240px;
311
+ --layout-circuit-traces: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='240' height='240' viewBox='0 0 240 240'><path d='M20 70L38 70L48.24 65.76L56 58' fill='none' stroke='%23000' stroke-width='1' stroke-opacity='0.22' stroke-linejoin='round' stroke-linecap='round' vector-effect='non-scaling-stroke'/><path d='M210 150L210 172L205.76 182.24L196 192' fill='none' stroke='%23000' stroke-width='1' stroke-opacity='0.22' stroke-linejoin='round' stroke-linecap='round' vector-effect='non-scaling-stroke'/><path d='M28 150L28 176' fill='none' stroke='%23000' stroke-width='1' stroke-opacity='0.22' stroke-linejoin='round' stroke-linecap='round' vector-effect='non-scaling-stroke'/><path d='M212 40L224 40' fill='none' stroke='%23000' stroke-width='1' stroke-opacity='0.22' stroke-linejoin='round' stroke-linecap='round' vector-effect='non-scaling-stroke'/><path d='M0 40L54 40L64.24 35.76L75.76 24.24L86 20L114 20L124.24 24.24L135.76 35.76L146 40L240 40' fill='none' stroke='%23000' stroke-width='1' stroke-opacity='0.6' stroke-linejoin='round' stroke-linecap='round' vector-effect='non-scaling-stroke'/><path d='M0 120L34 120L44.24 115.76L55.76 104.24L66 100L94 100L104.24 104.24L115.76 115.76L126 120L154 120L164.24 124.24L175.76 135.76L186 140L194 140L204.24 135.76L215.76 124.24L226 120L240 120' fill='none' stroke='%23000' stroke-width='1.5' stroke-opacity='0.9' stroke-linejoin='round' stroke-linecap='round' vector-effect='non-scaling-stroke'/><path d='M0 200L94 200L104.24 204.24L115.76 215.76L126 220L154 220L164.24 215.76L175.76 204.24L186 200L240 200' fill='none' stroke='%23000' stroke-width='1' stroke-opacity='0.6' stroke-linejoin='round' stroke-linecap='round' vector-effect='non-scaling-stroke'/><path d='M60 0L60 54L64.24 64.24L75.76 75.76L80 86L80 154L75.76 164.24L64.24 175.76L60 186L60 240' fill='none' stroke='%23000' stroke-width='1' stroke-opacity='0.55' stroke-linejoin='round' stroke-linecap='round' vector-effect='non-scaling-stroke'/><path d='M180 0L180 64L175.76 74.24L164.24 85.76L160 96L160 144L164.24 154.24L175.76 165.76L180 176L180 240' fill='none' stroke='%23000' stroke-width='1' stroke-opacity='0.55' stroke-linejoin='round' stroke-linecap='round' vector-effect='non-scaling-stroke'/><path d='M100 100L100 64' fill='none' stroke='%23000' stroke-width='1' stroke-opacity='0.4' stroke-linejoin='round' stroke-linecap='round' vector-effect='non-scaling-stroke'/><path d='M160 120L194 120L200 114L200 84' fill='none' stroke='%23000' stroke-width='1' stroke-opacity='0.4' stroke-linejoin='round' stroke-linecap='round' vector-effect='non-scaling-stroke'/><path d='M80 120L80 96' fill='none' stroke='%23000' stroke-width='1' stroke-opacity='0.4' stroke-linejoin='round' stroke-linecap='round' vector-effect='non-scaling-stroke'/><path d='M140 200L140 164' fill='none' stroke='%23000' stroke-width='1' stroke-opacity='0.4' stroke-linejoin='round' stroke-linecap='round' vector-effect='non-scaling-stroke'/></svg>");
312
+ --layout-circuit-nodes: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='240' height='240' viewBox='0 0 240 240'><circle cx='60' cy='40' r='3' fill='%23000' fill-opacity='0.85'/><circle cx='140' cy='40' r='3' fill='%23000' fill-opacity='0.85'/><circle cx='100' cy='200' r='3' fill='%23000' fill-opacity='0.85'/><circle cx='180' cy='200' r='3' fill='%23000' fill-opacity='0.85'/><circle cx='60' cy='100' r='3' fill='%23000' fill-opacity='0.85'/><circle cx='120' cy='120' r='3.2' fill='%23000' fill-opacity='0.85'/><circle cx='180' cy='140' r='3' fill='%23000' fill-opacity='0.85'/><circle cx='80' cy='80' r='3' fill='%23000' fill-opacity='0.85'/><circle cx='80' cy='160' r='3' fill='%23000' fill-opacity='0.85'/><circle cx='160' cy='90' r='3' fill='%23000' fill-opacity='0.85'/><circle cx='160' cy='150' r='3' fill='%23000' fill-opacity='0.85'/><circle cx='100' cy='100' r='2.6' fill='%23000' fill-opacity='0.85'/><circle cx='200' cy='120' r='2.6' fill='%23000' fill-opacity='0.85'/><circle cx='140' cy='200' r='2.6' fill='%23000' fill-opacity='0.85'/><circle cx='100' cy='60' r='6' fill='none' stroke='%23000' stroke-width='1.2' stroke-opacity='0.7' vector-effect='non-scaling-stroke'/><circle cx='100' cy='60' r='2' fill='%23000' fill-opacity='0.85'/><circle cx='200' cy='80' r='6' fill='none' stroke='%23000' stroke-width='1.2' stroke-opacity='0.7' vector-effect='non-scaling-stroke'/><circle cx='200' cy='80' r='2' fill='%23000' fill-opacity='0.85'/><circle cx='140' cy='160' r='6' fill='none' stroke='%23000' stroke-width='1.2' stroke-opacity='0.7' vector-effect='non-scaling-stroke'/><circle cx='140' cy='160' r='2' fill='%23000' fill-opacity='0.85'/><circle cx='40' cy='160' r='4.5' fill='none' stroke='%23000' stroke-width='1' stroke-opacity='0.55' vector-effect='non-scaling-stroke'/><circle cx='215' cy='40' r='4' fill='none' stroke='%23000' stroke-width='1' stroke-opacity='0.55' vector-effect='non-scaling-stroke'/><circle cx='25' cy='200' r='4' fill='none' stroke='%23000' stroke-width='1' stroke-opacity='0.55' vector-effect='non-scaling-stroke'/></svg>");
313
+ }
314
+
315
+ .layout-surface-circuit::before,
316
+ .layout-surface-circuit::after {
317
+ content: '';
318
+ position: absolute;
319
+ inset: 0;
320
+ z-index: 0;
321
+ pointer-events: none;
322
+ -webkit-mask-repeat: repeat;
323
+ mask-repeat: repeat;
324
+ -webkit-mask-position: center;
325
+ mask-position: center;
326
+ -webkit-mask-size: var(--layout-circuit-size);
327
+ mask-size: var(--layout-circuit-size);
328
+ }
329
+
330
+ .layout-surface-circuit::before {
331
+ background-color: hsl(var(--layout-grid-line) / var(--layout-circuit-opacity));
332
+ -webkit-mask-image: var(--layout-circuit-traces);
333
+ mask-image: var(--layout-circuit-traces);
334
+ }
335
+
336
+ .layout-surface-circuit::after {
337
+ background-color: hsl(var(--primary) / var(--layout-circuit-node-opacity));
338
+ -webkit-mask-image: var(--layout-circuit-nodes);
339
+ mask-image: var(--layout-circuit-nodes);
340
+ }
129
341
  }