@ojiepermana/angular-theme 22.0.35 → 22.0.41

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 (64) hide show
  1. package/README.md +54 -22
  2. package/fesm2022/ojiepermana-angular-theme-layout-wrapper.mjs +74 -20
  3. package/fesm2022/ojiepermana-angular-theme-layout.mjs +14 -2
  4. package/fesm2022/ojiepermana-angular-theme-page.mjs +251 -101
  5. package/fesm2022/ojiepermana-angular-theme-styles.mjs +375 -53
  6. package/layout/README.md +21 -19
  7. package/package.json +3 -3
  8. package/page/README.md +53 -15
  9. package/styles/README.md +18 -3
  10. package/styles/css/{seasonal/base → base}/tailwind.css +9 -5
  11. package/styles/css/{seasonal/base → base}/theme.css +27 -52
  12. package/styles/css/{seasonal/base → base}/tokens.css +62 -44
  13. package/styles/css/color/amber.css +52 -0
  14. package/styles/css/color/blue.css +52 -0
  15. package/styles/css/color/brand.css +16 -0
  16. package/styles/css/color/cyan.css +52 -0
  17. package/styles/css/color/emerald.css +52 -0
  18. package/styles/css/color/fuchsia.css +52 -0
  19. package/styles/css/color/green.css +52 -0
  20. package/styles/css/color/index.css +19 -0
  21. package/styles/css/color/indigo.css +52 -0
  22. package/styles/css/color/lime.css +52 -0
  23. package/styles/css/color/orange.css +52 -0
  24. package/styles/css/color/pink.css +52 -0
  25. package/styles/css/color/purple.css +52 -0
  26. package/styles/css/color/red.css +52 -0
  27. package/styles/css/color/rose.css +52 -0
  28. package/styles/css/color/sky.css +52 -0
  29. package/styles/css/color/teal.css +52 -0
  30. package/styles/css/color/violet.css +52 -0
  31. package/styles/css/color/yellow.css +52 -0
  32. package/styles/css/index.css +15 -6
  33. package/styles/css/neutral/gray.css +36 -0
  34. package/styles/css/neutral/index.css +11 -0
  35. package/styles/css/neutral/mauve.css +36 -0
  36. package/styles/css/neutral/mist.css +36 -0
  37. package/styles/css/neutral/neutral.css +36 -0
  38. package/styles/css/neutral/olive.css +36 -0
  39. package/styles/css/neutral/slate.css +36 -0
  40. package/styles/css/neutral/stone.css +36 -0
  41. package/styles/css/neutral/taupe.css +36 -0
  42. package/styles/css/neutral/zinc.css +36 -0
  43. package/styles/css/radius/index.css +29 -0
  44. package/styles/css/space/index.css +24 -0
  45. package/types/ojiepermana-angular-theme-layout-wrapper.d.ts +43 -10
  46. package/types/ojiepermana-angular-theme-layout.d.ts +1 -0
  47. package/types/ojiepermana-angular-theme-page.d.ts +88 -36
  48. package/types/ojiepermana-angular-theme-styles.d.ts +169 -37
  49. package/styles/css/seasonal/ied/package.css +0 -4
  50. package/styles/css/seasonal/ied/theme.css +0 -78
  51. package/styles/css/seasonal/imlek/components.css +0 -87
  52. package/styles/css/seasonal/imlek/package.css +0 -6
  53. package/styles/css/seasonal/imlek/tailwind.css +0 -144
  54. package/styles/css/seasonal/imlek/theme.css +0 -95
  55. package/styles/css/seasonal/imlek/tokens.css +0 -152
  56. package/styles/css/seasonal/index.css +0 -6
  57. package/styles/css/seasonal/natal/package.css +0 -4
  58. package/styles/css/seasonal/natal/theme.css +0 -78
  59. package/styles/css/seasonal/new-year/package.css +0 -4
  60. package/styles/css/seasonal/new-year/theme.css +0 -78
  61. package/styles/css/seasonal/ramadhan/package.css +0 -4
  62. package/styles/css/seasonal/ramadhan/theme.css +0 -78
  63. /package/styles/css/{seasonal/base → base}/components.css +0 -0
  64. /package/styles/css/{seasonal/base → base}/package.css +0 -0
@@ -7,25 +7,103 @@ const THEME_OPTIONS = new InjectionToken('THEME_OPTIONS');
7
7
 
8
8
  const THEME_MODES = ['light', 'dark', 'system'];
9
9
  const RESOLVED_THEME_MODES = ['light', 'dark'];
10
- const THEME_SEASONS = ['base', 'imlek', 'ramadhan', 'ied', 'natal', 'new-year'];
10
+ /**
11
+ * FluxUI-style accent palettes (axis `theme-color`). `base` = core theme (no
12
+ * override); `brand` = the consumer's `--brand` color. Others re-tint the full
13
+ * palette. Order mirrors the FluxUI accent picker (base…rose) + `brand`.
14
+ */
15
+ const THEME_COLORS = [
16
+ 'base',
17
+ 'red',
18
+ 'orange',
19
+ 'amber',
20
+ 'yellow',
21
+ 'lime',
22
+ 'green',
23
+ 'emerald',
24
+ 'teal',
25
+ 'cyan',
26
+ 'sky',
27
+ 'blue',
28
+ 'indigo',
29
+ 'violet',
30
+ 'purple',
31
+ 'fuchsia',
32
+ 'pink',
33
+ 'rose',
34
+ 'brand',
35
+ ];
36
+ /**
37
+ * Neutral (gray) families (axis `theme-neutral`). `base` = core theme neutrals.
38
+ * Overrides only the gray family, composing with any accent.
39
+ */
40
+ const THEME_NEUTRALS = [
41
+ 'base',
42
+ 'slate',
43
+ 'gray',
44
+ 'zinc',
45
+ 'neutral',
46
+ 'stone',
47
+ 'mauve',
48
+ 'olive',
49
+ 'mist',
50
+ 'taupe',
51
+ ];
52
+ /**
53
+ * Corner radius presets (axis `theme-radius`). Each drives the single
54
+ * `--radius-base` knob; the whole `--radius-*` scale + `rounded-*` utilities
55
+ * follow. `md` = the 0.625rem default; `full` is the pill extreme.
56
+ */
57
+ const THEME_RADII = ['none', 'sm', 'md', 'lg', 'xl', 'full'];
58
+ /**
59
+ * Spacing density presets (axis `theme-space`). Each drives the single
60
+ * `--spacing-base` knob; every `p-*`/`m-*`/`gap-*`/`w-*`/`h-*` utility follows.
61
+ * `normal` = the 0.25rem default (no-op baseline).
62
+ */
63
+ const THEME_SPACES = ['compact', 'normal', 'relaxed', 'spacious'];
11
64
  const DEFAULT_THEME_MODE = 'system';
12
- const DEFAULT_THEME_SEASON = 'base';
65
+ const DEFAULT_THEME_COLOR = 'base';
66
+ const DEFAULT_THEME_NEUTRAL = 'base';
67
+ const DEFAULT_THEME_RADIUS = 'md';
68
+ const DEFAULT_THEME_SPACE = 'normal';
13
69
  const THEME_MODE_STORAGE_KEY = 'theme-mode';
14
- const THEME_SEASON_STORAGE_KEY = 'theme-season';
70
+ const THEME_COLOR_STORAGE_KEY = 'theme-color';
71
+ const THEME_NEUTRAL_STORAGE_KEY = 'theme-neutral';
72
+ const THEME_BRAND_STORAGE_KEY = 'theme-brand';
73
+ const THEME_RADIUS_STORAGE_KEY = 'theme-radius';
74
+ const THEME_SPACE_STORAGE_KEY = 'theme-space';
15
75
  function isThemeMode(value) {
16
76
  return typeof value === 'string' && THEME_MODES.includes(value);
17
77
  }
18
78
  function isResolvedThemeMode(value) {
19
79
  return typeof value === 'string' && RESOLVED_THEME_MODES.includes(value);
20
80
  }
21
- function isThemeSeason(value) {
22
- return typeof value === 'string' && THEME_SEASONS.includes(value);
81
+ function isThemeColor(value) {
82
+ return typeof value === 'string' && THEME_COLORS.includes(value);
83
+ }
84
+ function isThemeNeutral(value) {
85
+ return typeof value === 'string' && THEME_NEUTRALS.includes(value);
86
+ }
87
+ function isThemeRadius(value) {
88
+ return typeof value === 'string' && THEME_RADII.includes(value);
89
+ }
90
+ function isThemeSpace(value) {
91
+ return typeof value === 'string' && THEME_SPACES.includes(value);
23
92
  }
24
93
  function normalizeThemeMode(value) {
25
94
  return isThemeMode(value) ? value : DEFAULT_THEME_MODE;
26
95
  }
27
- function normalizeThemeSeason(value) {
28
- return isThemeSeason(value) ? value : DEFAULT_THEME_SEASON;
96
+ function normalizeThemeColor(value) {
97
+ return isThemeColor(value) ? value : DEFAULT_THEME_COLOR;
98
+ }
99
+ function normalizeThemeNeutral(value) {
100
+ return isThemeNeutral(value) ? value : DEFAULT_THEME_NEUTRAL;
101
+ }
102
+ function normalizeThemeRadius(value) {
103
+ return isThemeRadius(value) ? value : DEFAULT_THEME_RADIUS;
104
+ }
105
+ function normalizeThemeSpace(value) {
106
+ return isThemeSpace(value) ? value : DEFAULT_THEME_SPACE;
29
107
  }
30
108
 
31
109
  class ThemeModeService {
@@ -127,67 +205,170 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.3", ngImpor
127
205
  type: Service
128
206
  }], ctorParameters: () => [] });
129
207
 
130
- class ThemeSeasonService {
208
+ const DEFAULT_BRAND_FOREGROUND = '0 0% 100%';
209
+ /**
210
+ * Consumer-configurable brand color. Writes `--brand` / `--brand-foreground` as
211
+ * inline custom properties on `<html>` so `bg-brand` / `text-brand-foreground`
212
+ * and the `theme-color='brand'` accent preset resolve to it.
213
+ *
214
+ * Default comes from `provideUiTheme({ brand })`; `setBrand()` overrides at
215
+ * runtime and persists. When unset, `--brand` falls back to `var(--primary)`.
216
+ */
217
+ class ThemeBrandService {
131
218
  documentRef = inject(DOCUMENT, { optional: true });
132
219
  options = inject(THEME_OPTIONS, { optional: true });
133
- seasonState = signal(DEFAULT_THEME_SEASON, /* @ts-ignore */
134
- ...(ngDevMode ? [{ debugName: "seasonState" }] : /* istanbul ignore next */ []));
135
- season = this.seasonState.asReadonly();
220
+ brandState = signal(null, /* @ts-ignore */
221
+ ...(ngDevMode ? [{ debugName: "brandState" }] : /* istanbul ignore next */ []));
222
+ brand = this.brandState.asReadonly();
136
223
  constructor() {
137
224
  this.ensureDefaults();
138
225
  effect(() => {
139
- const season = this.seasonState();
140
- this.persistSeason(season);
141
- this.applySeasonAttribute(season);
142
- this.ensureSeasonStylesheet(season);
226
+ const brand = this.brandState();
227
+ this.persist(brand);
228
+ this.applyBrand(brand);
143
229
  });
144
230
  }
145
- setSeason(season) {
146
- this.seasonState.set(season);
231
+ /** Set the brand color. `color`/`foreground` are HSL triplets (e.g. `'221 83% 53%'`). */
232
+ setBrand(color, foreground = DEFAULT_BRAND_FOREGROUND) {
233
+ this.brandState.set({ color, foreground });
234
+ }
235
+ /** Clear the consumer brand → `--brand` falls back to `var(--primary)`. */
236
+ clearBrand() {
237
+ this.brandState.set(null);
147
238
  }
148
239
  ensureDefaults() {
149
- const defaultSeason = normalizeThemeSeason(this.options?.season);
150
- const storedSeason = this.readStorage(THEME_SEASON_STORAGE_KEY) ?? defaultSeason;
151
- this.seasonState.set(normalizeThemeSeason(storedSeason));
240
+ const stored = this.readStoredBrand();
241
+ if (stored) {
242
+ this.brandState.set(stored);
243
+ return;
244
+ }
245
+ const fromOptions = this.normalizeOption(this.options?.brand);
246
+ if (fromOptions) {
247
+ this.brandState.set(fromOptions);
248
+ }
152
249
  }
153
- persistSeason(season) {
154
- this.writeStorage(THEME_SEASON_STORAGE_KEY, season);
250
+ normalizeOption(brand) {
251
+ if (!brand) {
252
+ return null;
253
+ }
254
+ if (typeof brand === 'string') {
255
+ return { color: brand, foreground: DEFAULT_BRAND_FOREGROUND };
256
+ }
257
+ return { color: brand.color, foreground: brand.foreground ?? DEFAULT_BRAND_FOREGROUND };
155
258
  }
156
- applySeasonAttribute(season) {
259
+ applyBrand(brand) {
157
260
  const root = this.documentRef?.documentElement;
158
261
  if (!root) {
159
262
  return;
160
263
  }
161
- root.setAttribute('theme-season', season);
162
- }
163
- /**
164
- * Lazily loads the active season's stylesheet via `<link>` when the app
165
- * configured `seasonalCssUrl`, so only base CSS ships in the main bundle.
166
- */
167
- ensureSeasonStylesheet(season) {
168
- const doc = this.documentRef;
169
- const resolveUrl = this.options?.seasonalCssUrl;
170
- if (!doc || !resolveUrl) {
264
+ if (!brand) {
265
+ root.style.removeProperty('--brand');
266
+ root.style.removeProperty('--brand-foreground');
171
267
  return;
172
268
  }
173
- const linkId = 'theme-season-styles';
174
- const existing = doc.getElementById(linkId);
175
- const url = season === 'base' ? null : resolveUrl(season);
176
- if (!url) {
177
- existing?.remove();
178
- return;
269
+ root.style.setProperty('--brand', brand.color);
270
+ root.style.setProperty('--brand-foreground', brand.foreground);
271
+ }
272
+ storage() {
273
+ try {
274
+ return this.documentRef?.defaultView?.localStorage ?? null;
275
+ }
276
+ catch {
277
+ return null;
179
278
  }
180
- if (existing) {
181
- if (existing.getAttribute('href') !== url) {
182
- existing.setAttribute('href', url);
279
+ }
280
+ readStoredBrand() {
281
+ try {
282
+ const raw = this.storage()?.getItem(THEME_BRAND_STORAGE_KEY);
283
+ if (!raw) {
284
+ return null;
183
285
  }
286
+ const parsed = JSON.parse(raw);
287
+ if (typeof parsed?.color === 'string') {
288
+ return {
289
+ color: parsed.color,
290
+ foreground: typeof parsed.foreground === 'string' ? parsed.foreground : DEFAULT_BRAND_FOREGROUND,
291
+ };
292
+ }
293
+ return null;
294
+ }
295
+ catch {
296
+ return null;
297
+ }
298
+ }
299
+ persist(brand) {
300
+ try {
301
+ const storage = this.storage();
302
+ if (!storage) {
303
+ return;
304
+ }
305
+ if (!brand) {
306
+ storage.removeItem(THEME_BRAND_STORAGE_KEY);
307
+ return;
308
+ }
309
+ storage.setItem(THEME_BRAND_STORAGE_KEY, JSON.stringify(brand));
310
+ }
311
+ catch {
184
312
  return;
185
313
  }
186
- const link = doc.createElement('link');
187
- link.id = linkId;
188
- link.rel = 'stylesheet';
189
- link.href = url;
190
- doc.head.appendChild(link);
314
+ }
315
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.3", ngImport: i0, type: ThemeBrandService, deps: [], target: i0.ɵɵFactoryTarget.Service });
316
+ static ɵprov = i0.ɵɵngDeclareService({ minVersion: "22.0.0", version: "22.0.3", ngImport: i0, type: ThemeBrandService });
317
+ }
318
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.3", ngImport: i0, type: ThemeBrandService, decorators: [{
319
+ type: Service
320
+ }], ctorParameters: () => [] });
321
+
322
+ /**
323
+ * Drives the two FluxUI-style color axes:
324
+ * - accent → `<html theme-color>` (full tinted palette per color)
325
+ * - neutral → `<html theme-neutral>` (gray family only)
326
+ *
327
+ * Both persist to localStorage and seed from `provideUiTheme({ color, neutral })`
328
+ * (a persisted choice wins over the configured default). `base` on either axis
329
+ * means "no override" — the core base theme applies.
330
+ */
331
+ class ThemeColorService {
332
+ documentRef = inject(DOCUMENT, { optional: true });
333
+ options = inject(THEME_OPTIONS, { optional: true });
334
+ colorState = signal(DEFAULT_THEME_COLOR, /* @ts-ignore */
335
+ ...(ngDevMode ? [{ debugName: "colorState" }] : /* istanbul ignore next */ []));
336
+ neutralState = signal(DEFAULT_THEME_NEUTRAL, /* @ts-ignore */
337
+ ...(ngDevMode ? [{ debugName: "neutralState" }] : /* istanbul ignore next */ []));
338
+ color = this.colorState.asReadonly();
339
+ neutral = this.neutralState.asReadonly();
340
+ constructor() {
341
+ this.ensureDefaults();
342
+ effect(() => {
343
+ const color = this.colorState();
344
+ this.persist(THEME_COLOR_STORAGE_KEY, color);
345
+ this.applyAttribute('theme-color', color);
346
+ });
347
+ effect(() => {
348
+ const neutral = this.neutralState();
349
+ this.persist(THEME_NEUTRAL_STORAGE_KEY, neutral);
350
+ this.applyAttribute('theme-neutral', neutral);
351
+ });
352
+ }
353
+ setColor(color) {
354
+ this.colorState.set(color);
355
+ }
356
+ setNeutral(neutral) {
357
+ this.neutralState.set(neutral);
358
+ }
359
+ ensureDefaults() {
360
+ // A configured brand with no explicit accent defaults the accent to 'brand'
361
+ // so the consumer's brand color becomes the primary (FluxUI-style).
362
+ const configuredColor = this.options?.color ?? (this.options?.brand ? 'brand' : undefined);
363
+ const defaultColor = normalizeThemeColor(configuredColor);
364
+ const storedColor = this.readStorage(THEME_COLOR_STORAGE_KEY) ?? defaultColor;
365
+ this.colorState.set(normalizeThemeColor(storedColor));
366
+ const defaultNeutral = normalizeThemeNeutral(this.options?.neutral);
367
+ const storedNeutral = this.readStorage(THEME_NEUTRAL_STORAGE_KEY) ?? defaultNeutral;
368
+ this.neutralState.set(normalizeThemeNeutral(storedNeutral));
369
+ }
370
+ applyAttribute(attribute, value) {
371
+ this.documentRef?.documentElement?.setAttribute(attribute, value);
191
372
  }
192
373
  storage() {
193
374
  try {
@@ -205,7 +386,7 @@ class ThemeSeasonService {
205
386
  return null;
206
387
  }
207
388
  }
208
- writeStorage(key, value) {
389
+ persist(key, value) {
209
390
  try {
210
391
  this.storage()?.setItem(key, value);
211
392
  }
@@ -213,10 +394,148 @@ class ThemeSeasonService {
213
394
  return;
214
395
  }
215
396
  }
216
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.3", ngImport: i0, type: ThemeSeasonService, deps: [], target: i0.ɵɵFactoryTarget.Service });
217
- static ɵprov = i0.ɵɵngDeclareService({ minVersion: "22.0.0", version: "22.0.3", ngImport: i0, type: ThemeSeasonService });
397
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.3", ngImport: i0, type: ThemeColorService, deps: [], target: i0.ɵɵFactoryTarget.Service });
398
+ static ɵprov = i0.ɵɵngDeclareService({ minVersion: "22.0.0", version: "22.0.3", ngImport: i0, type: ThemeColorService });
399
+ }
400
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.3", ngImport: i0, type: ThemeColorService, decorators: [{
401
+ type: Service
402
+ }], ctorParameters: () => [] });
403
+
404
+ /**
405
+ * Corner radius axis. Writes `<html theme-radius>`; each preset CSS sets the
406
+ * single `--radius-base` knob so the whole `--radius-*` scale + `rounded-*`
407
+ * utilities rescale. Persists to localStorage and seeds from
408
+ * `provideUiTheme({ radius })` (a persisted choice wins over the default).
409
+ */
410
+ class ThemeRadiusService {
411
+ documentRef = inject(DOCUMENT, { optional: true });
412
+ options = inject(THEME_OPTIONS, { optional: true });
413
+ radiusState = signal(DEFAULT_THEME_RADIUS, /* @ts-ignore */
414
+ ...(ngDevMode ? [{ debugName: "radiusState" }] : /* istanbul ignore next */ []));
415
+ radius = this.radiusState.asReadonly();
416
+ radiusPx = computed(() => {
417
+ switch (this.radius()) {
418
+ case 'none':
419
+ return 0;
420
+ case 'sm':
421
+ return 6;
422
+ case 'md':
423
+ return 10;
424
+ case 'lg':
425
+ return 14;
426
+ case 'xl':
427
+ return 20;
428
+ case 'full':
429
+ return 9999;
430
+ default:
431
+ return 10;
432
+ }
433
+ }, /* @ts-ignore */
434
+ ...(ngDevMode ? [{ debugName: "radiusPx" }] : /* istanbul ignore next */ []));
435
+ constructor() {
436
+ this.ensureDefaults();
437
+ effect(() => {
438
+ const radius = this.radiusState();
439
+ this.persist(radius);
440
+ this.documentRef?.documentElement?.setAttribute('theme-radius', radius);
441
+ });
442
+ }
443
+ setRadius(radius) {
444
+ this.radiusState.set(radius);
445
+ }
446
+ ensureDefaults() {
447
+ const fallback = normalizeThemeRadius(this.options?.radius);
448
+ const stored = this.readStorage() ?? fallback;
449
+ this.radiusState.set(normalizeThemeRadius(stored));
450
+ }
451
+ storage() {
452
+ try {
453
+ return this.documentRef?.defaultView?.localStorage ?? null;
454
+ }
455
+ catch {
456
+ return null;
457
+ }
458
+ }
459
+ readStorage() {
460
+ try {
461
+ return this.storage()?.getItem(THEME_RADIUS_STORAGE_KEY) ?? null;
462
+ }
463
+ catch {
464
+ return null;
465
+ }
466
+ }
467
+ persist(radius) {
468
+ try {
469
+ this.storage()?.setItem(THEME_RADIUS_STORAGE_KEY, radius);
470
+ }
471
+ catch {
472
+ return;
473
+ }
474
+ }
475
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.3", ngImport: i0, type: ThemeRadiusService, deps: [], target: i0.ɵɵFactoryTarget.Service });
476
+ static ɵprov = i0.ɵɵngDeclareService({ minVersion: "22.0.0", version: "22.0.3", ngImport: i0, type: ThemeRadiusService });
477
+ }
478
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.3", ngImport: i0, type: ThemeRadiusService, decorators: [{
479
+ type: Service
480
+ }], ctorParameters: () => [] });
481
+
482
+ /**
483
+ * Spacing density axis. Writes `<html theme-space>`; each preset CSS sets the
484
+ * single `--spacing-base` knob, which Tailwind's `--spacing` derives from, so
485
+ * every `p-*`/`m-*`/`gap-*`/`w-*`/`h-*` utility rescales. Persists to
486
+ * localStorage and seeds from `provideUiTheme({ space })` (a persisted choice
487
+ * wins over the default).
488
+ */
489
+ class ThemeSpaceService {
490
+ documentRef = inject(DOCUMENT, { optional: true });
491
+ options = inject(THEME_OPTIONS, { optional: true });
492
+ spaceState = signal(DEFAULT_THEME_SPACE, /* @ts-ignore */
493
+ ...(ngDevMode ? [{ debugName: "spaceState" }] : /* istanbul ignore next */ []));
494
+ space = this.spaceState.asReadonly();
495
+ constructor() {
496
+ this.ensureDefaults();
497
+ effect(() => {
498
+ const space = this.spaceState();
499
+ this.persist(space);
500
+ this.documentRef?.documentElement?.setAttribute('theme-space', space);
501
+ });
502
+ }
503
+ setSpace(space) {
504
+ this.spaceState.set(space);
505
+ }
506
+ ensureDefaults() {
507
+ const fallback = normalizeThemeSpace(this.options?.space);
508
+ const stored = this.readStorage() ?? fallback;
509
+ this.spaceState.set(normalizeThemeSpace(stored));
510
+ }
511
+ storage() {
512
+ try {
513
+ return this.documentRef?.defaultView?.localStorage ?? null;
514
+ }
515
+ catch {
516
+ return null;
517
+ }
518
+ }
519
+ readStorage() {
520
+ try {
521
+ return this.storage()?.getItem(THEME_SPACE_STORAGE_KEY) ?? null;
522
+ }
523
+ catch {
524
+ return null;
525
+ }
526
+ }
527
+ persist(space) {
528
+ try {
529
+ this.storage()?.setItem(THEME_SPACE_STORAGE_KEY, space);
530
+ }
531
+ catch {
532
+ return;
533
+ }
534
+ }
535
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.3", ngImport: i0, type: ThemeSpaceService, deps: [], target: i0.ɵɵFactoryTarget.Service });
536
+ static ɵprov = i0.ɵɵngDeclareService({ minVersion: "22.0.0", version: "22.0.3", ngImport: i0, type: ThemeSpaceService });
218
537
  }
219
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.3", ngImport: i0, type: ThemeSeasonService, decorators: [{
538
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.3", ngImport: i0, type: ThemeSpaceService, decorators: [{
220
539
  type: Service
221
540
  }], ctorParameters: () => [] });
222
541
 
@@ -235,7 +554,10 @@ function provideUiTheme(options = {}) {
235
554
  inject(MaterialSymbolsService).ensureLoaded();
236
555
  }
237
556
  inject(ThemeModeService);
238
- inject(ThemeSeasonService);
557
+ inject(ThemeColorService);
558
+ inject(ThemeBrandService);
559
+ inject(ThemeRadiusService);
560
+ inject(ThemeSpaceService);
239
561
  }),
240
562
  ]);
241
563
  }
@@ -244,4 +566,4 @@ function provideUiTheme(options = {}) {
244
566
  * Generated bundle index. Do not edit.
245
567
  */
246
568
 
247
- export { DEFAULT_THEME_MODE, DEFAULT_THEME_SEASON, RESOLVED_THEME_MODES, THEME_MODES, THEME_MODE_STORAGE_KEY, THEME_OPTIONS, THEME_SEASONS, THEME_SEASON_STORAGE_KEY, ThemeModeService, ThemeSeasonService, isResolvedThemeMode, isThemeMode, isThemeSeason, normalizeThemeMode, normalizeThemeSeason, provideUiTheme };
569
+ export { DEFAULT_THEME_COLOR, DEFAULT_THEME_MODE, DEFAULT_THEME_NEUTRAL, DEFAULT_THEME_RADIUS, DEFAULT_THEME_SPACE, RESOLVED_THEME_MODES, THEME_BRAND_STORAGE_KEY, THEME_COLORS, THEME_COLOR_STORAGE_KEY, THEME_MODES, THEME_MODE_STORAGE_KEY, THEME_NEUTRALS, THEME_NEUTRAL_STORAGE_KEY, THEME_OPTIONS, THEME_RADII, THEME_RADIUS_STORAGE_KEY, THEME_SPACES, THEME_SPACE_STORAGE_KEY, ThemeBrandService, ThemeColorService, ThemeModeService, ThemeRadiusService, ThemeSpaceService, isResolvedThemeMode, isThemeColor, isThemeMode, isThemeNeutral, isThemeRadius, isThemeSpace, normalizeThemeColor, normalizeThemeMode, normalizeThemeNeutral, normalizeThemeRadius, normalizeThemeSpace, provideUiTheme };
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ojiepermana/angular-theme",
3
- "version": "22.0.35",
3
+ "version": "22.0.41",
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.35",
17
- "@ojiepermana/angular-component": "^22.0.35",
16
+ "@ojiepermana/angular-navigation": "^22.0.41",
17
+ "@ojiepermana/angular-component": "^22.0.41",
18
18
  "rxjs": ">=7.8.0"
19
19
  },
20
20
  "dependencies": {