@makigamestudio/ui-ionic 0.8.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 +675 -32
- package/fesm2022/makigamestudio-ui-ionic.mjs +9 -9
- package/fesm2022/makigamestudio-ui-ionic.mjs.map +1 -1
- package/global-styles.css +68 -0
- package/global-styles.scss +87 -0
- package/package.json +2 -2
- package/theme.css +65 -89
- package/theme.scss +33 -162
package/README.md
CHANGED
|
@@ -1,64 +1,707 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @makigamestudio/ui-ionic
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Ionic 8 component implementations for the ui-core library. This package provides ready-to-use Angular 21 standalone components built with Ionic Framework.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Features
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
- **Zoneless Change Detection** — No zone.js dependency
|
|
8
|
+
- **Signal-Based Architecture** — Angular Signals for all reactivity
|
|
9
|
+
- **Standalone Components** — No NgModules required
|
|
10
|
+
- **Ionic 8 Integration** — Native Ionic components and styling
|
|
11
|
+
- **TypeScript Strict Mode** — Full type safety
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
8
14
|
|
|
9
15
|
```bash
|
|
10
|
-
|
|
16
|
+
npm install @makigamestudio/ui-ionic @makigamestudio/ui-core
|
|
11
17
|
```
|
|
12
18
|
|
|
13
|
-
|
|
19
|
+
### Peer Dependencies
|
|
14
20
|
|
|
15
|
-
```
|
|
16
|
-
|
|
21
|
+
```json
|
|
22
|
+
{
|
|
23
|
+
"@angular/core": "^21.0.0",
|
|
24
|
+
"@ionic/angular": "^8.0.0",
|
|
25
|
+
"@makigamestudio/ui-core": "^0.5.0"
|
|
26
|
+
}
|
|
17
27
|
```
|
|
18
28
|
|
|
19
|
-
##
|
|
29
|
+
## Theming
|
|
20
30
|
|
|
21
|
-
|
|
31
|
+
### Quick Start
|
|
22
32
|
|
|
23
|
-
|
|
24
|
-
|
|
33
|
+
Import the theme file in your global styles to get both CSS variables and component styles:
|
|
34
|
+
|
|
35
|
+
```scss
|
|
36
|
+
// src/global.scss or src/styles.scss
|
|
37
|
+
@use '@makigamestudio/ui-ionic/theme.scss';
|
|
38
|
+
|
|
39
|
+
// Your custom overrides AFTER the import
|
|
40
|
+
:root {
|
|
41
|
+
--maki-spacing-md: 16px; // Override default spacing
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
**Note:** Importing `theme.scss` automatically includes `global-styles.scss`, so you only need one import.
|
|
46
|
+
|
|
47
|
+
### Theming Strategy
|
|
48
|
+
|
|
49
|
+
This library follows a **hybrid approach** for maximum flexibility:
|
|
50
|
+
|
|
51
|
+
1. **Uses Ionic's color system** — All components use `--ion-color-*` variables (primary, secondary, success, etc.) so they automatically match your app's Ionic theme.
|
|
52
|
+
|
|
53
|
+
2. **Defines custom design tokens** — Only for properties that Ionic doesn't provide: spacing, border radius, shadows, and typography.
|
|
54
|
+
|
|
55
|
+
This means when you change your Ionic theme colors, all ui-ionic components automatically adapt. No duplicate configuration needed!
|
|
56
|
+
|
|
57
|
+
### Import Order
|
|
58
|
+
|
|
59
|
+
Always import the theme **before** your custom overrides:
|
|
60
|
+
|
|
61
|
+
```scss
|
|
62
|
+
// ✅ CORRECT ORDER
|
|
63
|
+
@use '@makigamestudio/ui-ionic/theme.scss';
|
|
64
|
+
|
|
65
|
+
:root {
|
|
66
|
+
--maki-spacing-lg: 20px;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ❌ WRONG ORDER (overrides won't work)
|
|
70
|
+
:root {
|
|
71
|
+
--maki-spacing-lg: 20px;
|
|
72
|
+
}
|
|
73
|
+
@use '@makigamestudio/ui-ionic/theme.scss'; // This resets your overrides!
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Available CSS Variables
|
|
77
|
+
|
|
78
|
+
All variables include fallback values for robustness.
|
|
79
|
+
|
|
80
|
+
#### Spacing Scale
|
|
81
|
+
|
|
82
|
+
```css
|
|
83
|
+
--maki-spacing-xs: 4px;
|
|
84
|
+
--maki-spacing-sm: 8px;
|
|
85
|
+
--maki-spacing-md: 12px;
|
|
86
|
+
--maki-spacing-lg: 16px;
|
|
87
|
+
--maki-spacing-xl: 24px;
|
|
88
|
+
--maki-spacing-xxl: 32px;
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
#### Border Radius Scale
|
|
92
|
+
|
|
93
|
+
```css
|
|
94
|
+
--maki-radius-xs: 4px;
|
|
95
|
+
--maki-radius-sm: 8px;
|
|
96
|
+
--maki-radius-md: 12px;
|
|
97
|
+
--maki-radius-lg: 16px;
|
|
98
|
+
--maki-radius-xl: 24px;
|
|
99
|
+
--maki-radius-full: 9999px;
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
#### Shadows
|
|
103
|
+
|
|
104
|
+
```css
|
|
105
|
+
--maki-box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
106
|
+
/* Dark mode: 0 2px 8px rgba(0, 0, 0, 0.3) */
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
#### Typography - Font Sizes
|
|
110
|
+
|
|
111
|
+
```css
|
|
112
|
+
--maki-font-size-xs: 0.75rem;
|
|
113
|
+
--maki-font-size-sm: 0.875rem;
|
|
114
|
+
--maki-font-size-md: 1rem;
|
|
115
|
+
--maki-font-size-lg: 1.125rem;
|
|
116
|
+
--maki-font-size-xl: 1.25rem;
|
|
117
|
+
--maki-font-size-xxl: 1.5rem;
|
|
118
|
+
--maki-font-size-xxxl: 2rem;
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
#### Typography - Line Heights
|
|
122
|
+
|
|
123
|
+
```css
|
|
124
|
+
--maki-line-height-tight: 1.25;
|
|
125
|
+
--maki-line-height-normal: 1.5;
|
|
126
|
+
--maki-line-height-relaxed: 1.75;
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
#### Typography - Letter Spacing
|
|
130
|
+
|
|
131
|
+
```css
|
|
132
|
+
--maki-letter-spacing-normal: 0;
|
|
133
|
+
--maki-letter-spacing-wide: 0.02em;
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
````
|
|
137
|
+
|
|
138
|
+
**Note:** Tooltip show/hide delays (250ms/200ms) are intentionally hardcoded in the service to maintain consistent UX and prevent consumers from setting extreme values that would break expected behavior.
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
**Z-Index Hierarchy:**
|
|
142
|
+
|
|
143
|
+
```
|
|
144
|
+
Modal (10000) ← Highest layer (blocks everything)
|
|
145
|
+
├─ Tooltip (9999) ← Above popovers, below modals
|
|
146
|
+
├─ Popover (9000) ← Dropdowns and context menus
|
|
147
|
+
├─ Sticky (1020) ← Sticky headers
|
|
148
|
+
└─ Dropdown (1000) ← Basic dropdowns
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
This hierarchy ensures tooltips appear above popovers/dropdowns but below modal backdrops.
|
|
152
|
+
|
|
153
|
+
### Customization Examples
|
|
154
|
+
|
|
155
|
+
#### Override Spacing
|
|
156
|
+
|
|
157
|
+
```scss
|
|
158
|
+
@use '@makigamestudio/ui-ionic/theme.scss';
|
|
159
|
+
|
|
160
|
+
:root {
|
|
161
|
+
--maki-spacing-md: 16px; // Change medium spacing
|
|
162
|
+
--maki-spacing-lg: 24px; // Change large spacing
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
#### Change Shadows
|
|
167
|
+
|
|
168
|
+
```scss
|
|
169
|
+
@use '@makigamestudio/ui-ionic/theme.scss';
|
|
170
|
+
|
|
171
|
+
:root {
|
|
172
|
+
--maki-box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); // Stronger shadow
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
#### Customize Border Radius
|
|
177
|
+
|
|
178
|
+
```scss
|
|
179
|
+
@use '@makigamestudio/ui-ionic/theme.scss';
|
|
180
|
+
|
|
181
|
+
:root {
|
|
182
|
+
--maki-radius-sm: 12px; // Rounder tooltips and cards
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### Shadow DOM and RTL Support
|
|
187
|
+
|
|
188
|
+
The library properly handles Ionic's Shadow DOM components and provides RTL (Right-to-Left) language support through CSS custom properties.
|
|
189
|
+
|
|
190
|
+
#### Styling Ionic Shadow DOM Components
|
|
191
|
+
|
|
192
|
+
Ionic components like `ion-button` use Shadow DOM, which prevents regular CSS from penetrating component boundaries. The library uses Ionic's exposed CSS custom properties (e.g., `--padding-start`, `--padding-end`) to style these components:
|
|
193
|
+
|
|
194
|
+
```typescript
|
|
195
|
+
// button.component.ts
|
|
196
|
+
ion-button {
|
|
197
|
+
--padding-start: var(--maki-spacing-sm, 0.5rem); // Maps to Shadow DOM
|
|
198
|
+
--padding-end: var(--maki-spacing-sm, 0.5rem);
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
**Why this pattern is correct:**
|
|
203
|
+
|
|
204
|
+
1. **Shadow DOM Encapsulation**: Regular `padding` CSS won't work on `ion-button` — you must use Ionic's custom properties
|
|
205
|
+
2. **Consistent Theming**: All spacing uses `--maki-*` tokens, ensuring layout themes affect Ionic components
|
|
206
|
+
3. **RTL Support**: `--padding-start`/`--padding-end` automatically flip in RTL languages (unlike `padding-left`/`padding-right`)
|
|
207
|
+
|
|
208
|
+
**RTL Behavior:**
|
|
209
|
+
|
|
210
|
+
```
|
|
211
|
+
LTR (English): --padding-start → left, --padding-end → right
|
|
212
|
+
RTL (Arabic): --padding-start → right, --padding-end → left
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### Theming Architecture
|
|
216
|
+
|
|
217
|
+
The library follows a **hybrid theming approach** that cleanly separates UI-agnostic logic from CSS-specific implementation:
|
|
218
|
+
|
|
219
|
+
#### Separation of Concerns
|
|
220
|
+
|
|
221
|
+
**ui-core (UI-Agnostic):**
|
|
222
|
+
|
|
223
|
+
- ✅ Pure computational logic (position calculations, visibility rules)
|
|
224
|
+
- ✅ State management with signals
|
|
225
|
+
- ✅ Behavioral timing (show/close delays remain hardcoded: 250ms/200ms)
|
|
226
|
+
- ❌ No CSS, no DOM manipulation, no framework-specific code
|
|
227
|
+
|
|
228
|
+
**ui-ionic (Ionic-Specific):**
|
|
229
|
+
|
|
230
|
+
- ✅ CSS variables and theming
|
|
231
|
+
- ✅ DOM manipulation and rendering
|
|
232
|
+
- ✅ Ionic component integration
|
|
233
|
+
- ✅ CSS transitions and animations
|
|
234
|
+
|
|
235
|
+
#### Timing Strategy
|
|
236
|
+
|
|
237
|
+
The library uses a **hybrid timing approach**:
|
|
238
|
+
|
|
239
|
+
**Behavioral Delays (Service-Controlled):**
|
|
240
|
+
|
|
241
|
+
```typescript
|
|
242
|
+
// ui-core/tooltip.service.ts - Hardcoded for consistent UX
|
|
243
|
+
getShowDelayMs(): number { return 250; } // Wait before showing tooltip
|
|
244
|
+
getCloseDelayMs(): number { return 200; } // Grace period before hiding
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
These values are intentionally hardcoded to prevent consumers from setting extreme values (e.g., 5000ms) that would break expected tooltip behavior.
|
|
248
|
+
|
|
249
|
+
**CSS Timing (Theme-Controlled):**
|
|
250
|
+
|
|
251
|
+
```scss
|
|
252
|
+
// ui-ionic/theme.scss - Themable for visual preferences
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
These values can be customized per layout theme (compact uses faster transitions, comfortable uses slower).
|
|
256
|
+
|
|
257
|
+
**Why This Separation:**
|
|
258
|
+
|
|
259
|
+
- **Behavioral delays** define UX patterns (how long to wait before showing)
|
|
260
|
+
- **CSS timing** defines visual polish (how fast animations feel)
|
|
261
|
+
- Mixing these concerns would make layout themes accidentally break tooltip interaction patterns
|
|
262
|
+
|
|
263
|
+
### Dark Mode Toggle
|
|
264
|
+
|
|
265
|
+
The library is **theme-agnostic** and works with both light and dark modes. Theme customization and dark mode implementation is the **consumer's responsibility**. The playground app demonstrates a complete dark theme implementation pattern.
|
|
266
|
+
|
|
267
|
+
#### 1. Create a Theme Toggle Service
|
|
268
|
+
|
|
269
|
+
Create a signal-based service to manage theme state:
|
|
270
|
+
|
|
271
|
+
```typescript
|
|
272
|
+
// services/theme-toggle.service.ts
|
|
273
|
+
import { effect, inject, Injectable, Renderer2, RendererFactory2, signal } from '@angular/core';
|
|
274
|
+
import { DOCUMENT } from '@angular/common';
|
|
275
|
+
|
|
276
|
+
export type ThemeMode = 'light' | 'dark';
|
|
277
|
+
|
|
278
|
+
@Injectable({ providedIn: 'root' })
|
|
279
|
+
export class ThemeToggleService {
|
|
280
|
+
private readonly document = inject(DOCUMENT);
|
|
281
|
+
private readonly renderer: Renderer2;
|
|
282
|
+
private readonly storageKey = 'theme-mode';
|
|
283
|
+
|
|
284
|
+
private readonly _colorThemeMode = signal<ThemeMode>(this.getInitialColorTheme());
|
|
285
|
+
readonly mode = this._colorThemeMode.asReadonly();
|
|
286
|
+
|
|
287
|
+
constructor() {
|
|
288
|
+
const rendererFactory = inject(RendererFactory2);
|
|
289
|
+
this.renderer = rendererFactory.createRenderer(null, null);
|
|
290
|
+
|
|
291
|
+
// Apply initial theme class
|
|
292
|
+
this.applyThemeClass(this._colorThemeMode());
|
|
293
|
+
|
|
294
|
+
// Batch localStorage writes using effect
|
|
295
|
+
effect(() => {
|
|
296
|
+
const currentMode = this._colorThemeMode();
|
|
297
|
+
try {
|
|
298
|
+
localStorage.setItem(this.storageKey, currentMode);
|
|
299
|
+
} catch (error) {
|
|
300
|
+
console.error('Failed to persist theme:', error);
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
toggleColorTheme(): void {
|
|
306
|
+
this.setColorTheme(this._colorThemeMode() === 'light' ? 'dark' : 'light');
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
setColorTheme(mode: ThemeMode): void {
|
|
310
|
+
this._colorThemeMode.set(mode);
|
|
311
|
+
this.applyThemeClass(mode);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
private getInitialColorTheme(): ThemeMode {
|
|
315
|
+
try {
|
|
316
|
+
const stored = localStorage.getItem(this.storageKey);
|
|
317
|
+
if (stored === 'light' || stored === 'dark') return stored;
|
|
318
|
+
} catch {}
|
|
319
|
+
|
|
320
|
+
// Fallback to prefers-color-scheme
|
|
321
|
+
if (this.document.defaultView?.matchMedia?.('(prefers-color-scheme: dark)').matches) {
|
|
322
|
+
return 'dark';
|
|
323
|
+
}
|
|
324
|
+
return 'light';
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
private applyThemeClass(mode: ThemeMode): void {
|
|
328
|
+
const body = this.document.body;
|
|
329
|
+
if (!body) return;
|
|
330
|
+
|
|
331
|
+
if (mode === 'dark') {
|
|
332
|
+
this.renderer.addClass(body, 'dark');
|
|
333
|
+
} else {
|
|
334
|
+
this.renderer.removeClass(body, 'dark');
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
#### 2. Define Dark Theme Variables
|
|
341
|
+
|
|
342
|
+
Create a dark theme stylesheet with Ionic's recommended dark palette:
|
|
343
|
+
|
|
344
|
+
```scss
|
|
345
|
+
// theme-dark.scss
|
|
346
|
+
body.dark {
|
|
347
|
+
// Ionic Core Colors (Dark Mode)
|
|
348
|
+
--ion-background-color: #121212;
|
|
349
|
+
--ion-text-color: #ffffff;
|
|
350
|
+
|
|
351
|
+
// Primary - Lighter for better contrast on dark backgrounds
|
|
352
|
+
--ion-color-primary: #428cff;
|
|
353
|
+
--ion-color-primary-contrast: #ffffff;
|
|
354
|
+
|
|
355
|
+
// Secondary
|
|
356
|
+
--ion-color-secondary: #50c8ff;
|
|
357
|
+
--ion-color-secondary-contrast: #ffffff;
|
|
358
|
+
|
|
359
|
+
// Success
|
|
360
|
+
--ion-color-success: #2fdf75;
|
|
361
|
+
--ion-color-success-contrast: #000000;
|
|
362
|
+
|
|
363
|
+
// Warning
|
|
364
|
+
--ion-color-warning: #ffd534;
|
|
365
|
+
--ion-color-warning-contrast: #000000;
|
|
366
|
+
|
|
367
|
+
// Danger
|
|
368
|
+
--ion-color-danger: #ff4961;
|
|
369
|
+
--ion-color-danger-contrast: #ffffff;
|
|
370
|
+
|
|
371
|
+
// Light - Inverted (becomes darker)
|
|
372
|
+
--ion-color-light: #222428;
|
|
373
|
+
--ion-color-light-contrast: #ffffff;
|
|
374
|
+
|
|
375
|
+
// Medium
|
|
376
|
+
--ion-color-medium: #989aa2;
|
|
377
|
+
--ion-color-medium-contrast: #000000;
|
|
378
|
+
|
|
379
|
+
// Dark - Inverted (becomes lighter)
|
|
380
|
+
--ion-color-dark: #f4f5f8;
|
|
381
|
+
--ion-color-dark-contrast: #000000;
|
|
382
|
+
|
|
383
|
+
// Maki Design Tokens
|
|
384
|
+
--maki-box-shadow: 0 2px 8px rgba(0, 0, 0, 0.5);
|
|
385
|
+
}
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
#### 3. Import Dark Theme
|
|
389
|
+
|
|
390
|
+
Add the dark theme import to your global styles:
|
|
391
|
+
|
|
392
|
+
```scss
|
|
393
|
+
// styles.scss
|
|
394
|
+
@use '@makigamestudio/ui-ionic/theme.scss';
|
|
395
|
+
@use './theme-dark.scss'; // Your dark theme overrides
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
#### 4. Add Theme Toggle UI
|
|
399
|
+
|
|
400
|
+
Use the theme service in your components:
|
|
401
|
+
|
|
402
|
+
```typescript
|
|
403
|
+
import { computed, inject } from '@angular/core';
|
|
404
|
+
import { IonToggle } from '@ionic/angular/standalone';
|
|
405
|
+
import { ThemeToggleService } from './services/theme-toggle.service';
|
|
406
|
+
|
|
407
|
+
@Component({
|
|
408
|
+
imports: [IonToggle],
|
|
409
|
+
template: `
|
|
410
|
+
<ion-toggle
|
|
411
|
+
[checked]="isDark()"
|
|
412
|
+
(ionChange)="onThemeToggle($event)"
|
|
413
|
+
aria-label="Toggle dark mode"
|
|
414
|
+
/>
|
|
415
|
+
`
|
|
416
|
+
})
|
|
417
|
+
export class MyComponent {
|
|
418
|
+
readonly themeService = inject(ThemeToggleService);
|
|
419
|
+
readonly isDark = computed(() => this.themeService.mode() === 'dark');
|
|
420
|
+
|
|
421
|
+
onThemeToggle(event: CustomEvent): void {
|
|
422
|
+
this.themeService.setColorTheme(event.detail.checked ? 'dark' : 'light');
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
#### Key Points
|
|
428
|
+
|
|
429
|
+
- **CSS Variables Required**: Always use CSS variables in your components (e.g., `var(--ion-color-primary)`) so they adapt automatically to theme changes
|
|
430
|
+
- **Class-Based Theming**: The service applies/removes the `dark` class on `<body>` to trigger theme overrides
|
|
431
|
+
- **Persistence**: Theme preference is saved to `localStorage` and restored on app load
|
|
432
|
+
- **Fallback Support**: Automatically detects `prefers-color-scheme: dark` if no saved preference exists
|
|
433
|
+
- **Signal-Based**: Fully reactive using Angular signals for zoneless compatibility
|
|
434
|
+
|
|
435
|
+
For a complete working example, see the [playground app](../../projects/playground/src/app/home/home.page.ts) source code.
|
|
436
|
+
|
|
437
|
+
### Layout Theme Toggle
|
|
438
|
+
|
|
439
|
+
In addition to color themes (light/dark), you can implement layout themes to adjust spacing, typography for different user preferences. The playground demonstrates three layout modes: **compact**, **default**, and **comfortable**.
|
|
440
|
+
|
|
441
|
+
#### Layout Theme Comparison
|
|
442
|
+
|
|
443
|
+
| Property | Compact | Default (Baseline) | Comfortable |
|
|
444
|
+
| ------------------ | ------- | ------------------ | ----------- |
|
|
445
|
+
| **Line Height** | | | |
|
|
446
|
+
| Tight | 1.2 | 1.25 | 1.4 |
|
|
447
|
+
| Normal | 1.3 | 1.5 | 1.75 |
|
|
448
|
+
| Relaxed | 1.5 | 1.75 | 2.0 |
|
|
449
|
+
| **Letter Spacing** | | | |
|
|
450
|
+
| Normal | 0 | 0 | 0 |
|
|
451
|
+
| Wide | 0.02em | 0.02em | 0.04em |
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
#### Layout Theme Options
|
|
455
|
+
|
|
456
|
+
- **Compact**: Reduced spacing (15-20% smaller), tighter line heights — ideal for information-dense interfaces or large screens
|
|
457
|
+
- **Default**: Standard spacing and typography (library baseline) — balanced for most use cases
|
|
458
|
+
- **Comfortable**: Increased spacing (20-25% larger), relaxed line heights, wider letter spacing — better accessibility and readability
|
|
459
|
+
|
|
460
|
+
#### 1. Create Layout Theme Stylesheets
|
|
461
|
+
|
|
462
|
+
Create separate SCSS files for each layout variant:
|
|
463
|
+
|
|
464
|
+
```scss
|
|
465
|
+
// theme-layout-compact.scss
|
|
466
|
+
body.layout-compact {
|
|
467
|
+
// Reduced spacing (20% smaller)
|
|
468
|
+
--maki-spacing-xxs: 0.1rem;
|
|
469
|
+
--maki-spacing-xs: 0.2rem;
|
|
470
|
+
--maki-spacing-sm: 0.4rem;
|
|
471
|
+
--maki-spacing-md: 0.6rem;
|
|
472
|
+
--maki-spacing-lg: 0.8rem;
|
|
473
|
+
--maki-spacing-xl: 1.2rem;
|
|
474
|
+
--maki-spacing-xxl: 1.6rem;
|
|
475
|
+
|
|
476
|
+
// Tighter typography
|
|
477
|
+
--maki-font-size-xs: 0.6875rem;
|
|
478
|
+
--maki-font-size-sm: 0.8125rem;
|
|
479
|
+
--maki-font-size-md: 0.9375rem;
|
|
480
|
+
// ... other font sizes
|
|
481
|
+
|
|
482
|
+
// Smaller border radius
|
|
483
|
+
--maki-radius-xs: 0.2rem;
|
|
484
|
+
--maki-radius-sm: 0.375rem;
|
|
485
|
+
// ... other radius values
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// theme-layout-comfortable.scss
|
|
489
|
+
body.layout-comfortable {
|
|
490
|
+
// Increased spacing (25% larger)
|
|
491
|
+
--maki-spacing-xxs: 0.15625rem;
|
|
492
|
+
--maki-spacing-xs: 0.3125rem;
|
|
493
|
+
--maki-spacing-sm: 0.625rem;
|
|
494
|
+
--maki-spacing-md: 0.9375rem;
|
|
495
|
+
--maki-spacing-lg: 1.25rem;
|
|
496
|
+
--maki-spacing-xl: 1.875rem;
|
|
497
|
+
--maki-spacing-xxl: 2.5rem;
|
|
498
|
+
|
|
499
|
+
// Larger typography for readability
|
|
500
|
+
--maki-font-size-xs: 0.8125rem;
|
|
501
|
+
--maki-font-size-sm: 0.9375rem;
|
|
502
|
+
--maki-font-size-md: 1.0625rem;
|
|
503
|
+
// ... other font sizes
|
|
504
|
+
|
|
505
|
+
// Larger border radius
|
|
506
|
+
--maki-radius-xs: 0.3125rem;
|
|
507
|
+
--maki-radius-sm: 0.625rem;
|
|
508
|
+
// ... other radius values
|
|
509
|
+
}
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
#### 2. Update Theme Service
|
|
513
|
+
|
|
514
|
+
Extend your `ThemeToggleService` to manage layout modes:
|
|
515
|
+
|
|
516
|
+
```typescript
|
|
517
|
+
export type LayoutMode = 'compact' | 'default' | 'comfortable';
|
|
518
|
+
|
|
519
|
+
@Injectable({ providedIn: 'root' })
|
|
520
|
+
export class ThemeToggleService {
|
|
521
|
+
private readonly layoutStorageKey = 'layout-mode';
|
|
522
|
+
private readonly _layoutThemeMode = signal<LayoutMode>(this.getInitialLayoutTheme());
|
|
523
|
+
readonly layoutThemeMode = this._layoutThemeMode.asReadonly();
|
|
524
|
+
|
|
525
|
+
constructor() {
|
|
526
|
+
// ... existing color theme setup
|
|
527
|
+
|
|
528
|
+
// Apply initial layout class
|
|
529
|
+
this.applyLayoutClass(this._layoutThemeMode());
|
|
530
|
+
|
|
531
|
+
// Batch layout localStorage writes
|
|
532
|
+
effect(() => {
|
|
533
|
+
const currentLayoutMode = this._layoutThemeMode();
|
|
534
|
+
try {
|
|
535
|
+
localStorage.setItem(this.layoutStorageKey, currentLayoutMode);
|
|
536
|
+
} catch (error) {
|
|
537
|
+
console.error('Failed to persist layout theme:', error);
|
|
538
|
+
}
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
setLayoutTheme(mode: LayoutMode): void {
|
|
543
|
+
this._layoutThemeMode.set(mode);
|
|
544
|
+
this.applyLayoutClass(mode);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
private getInitialLayoutTheme(): LayoutMode {
|
|
548
|
+
try {
|
|
549
|
+
const stored = localStorage.getItem(this.layoutStorageKey);
|
|
550
|
+
if (stored === 'compact' || stored === 'default' || stored === 'comfortable') {
|
|
551
|
+
return stored;
|
|
552
|
+
}
|
|
553
|
+
} catch {}
|
|
554
|
+
return 'default';
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
private applyLayoutClass(mode: LayoutMode): void {
|
|
558
|
+
const body = this.document.body;
|
|
559
|
+
if (!body) return;
|
|
560
|
+
|
|
561
|
+
// Remove all layout classes
|
|
562
|
+
this.renderer.removeClass(body, 'layout-compact');
|
|
563
|
+
this.renderer.removeClass(body, 'layout-comfortable');
|
|
564
|
+
|
|
565
|
+
// Apply appropriate class (default has no class)
|
|
566
|
+
if (mode === 'compact') {
|
|
567
|
+
this.renderer.addClass(body, 'layout-compact');
|
|
568
|
+
} else if (mode === 'comfortable') {
|
|
569
|
+
this.renderer.addClass(body, 'layout-comfortable');
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
#### 3. Import Layout Themes
|
|
576
|
+
|
|
577
|
+
Add layout theme imports to your global styles:
|
|
578
|
+
|
|
579
|
+
```scss
|
|
580
|
+
// styles.scss
|
|
581
|
+
@use '@makigamestudio/ui-ionic/theme.scss';
|
|
582
|
+
@use './theme-dark.scss';
|
|
583
|
+
@use './theme-layout-compact.scss';
|
|
584
|
+
@use './theme-layout-comfortable.scss';
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
#### 4. Add Layout Switcher UI
|
|
588
|
+
|
|
589
|
+
Use `ion-segment` for layout mode selection:
|
|
590
|
+
|
|
591
|
+
```typescript
|
|
592
|
+
import { computed, inject } from '@angular/core';
|
|
593
|
+
import { IonSegment, IonSegmentButton, IonLabel } from '@ionic/angular/standalone';
|
|
594
|
+
|
|
595
|
+
@Component({
|
|
596
|
+
imports: [IonSegment, IonSegmentButton, IonLabel],
|
|
597
|
+
template: `
|
|
598
|
+
<ion-segment [value]="layoutThemeMode()" (ionChange)="onLayoutChange($event)">
|
|
599
|
+
<ion-segment-button value="compact">
|
|
600
|
+
<ion-label>Compact</ion-label>
|
|
601
|
+
</ion-segment-button>
|
|
602
|
+
<ion-segment-button value="default">
|
|
603
|
+
<ion-label>Default</ion-label>
|
|
604
|
+
</ion-segment-button>
|
|
605
|
+
<ion-segment-button value="comfortable">
|
|
606
|
+
<ion-label>Comfortable</ion-label>
|
|
607
|
+
</ion-segment-button>
|
|
608
|
+
</ion-segment>
|
|
609
|
+
`
|
|
610
|
+
})
|
|
611
|
+
export class MyComponent {
|
|
612
|
+
readonly themeService = inject(ThemeToggleService);
|
|
613
|
+
readonly layoutThemeMode = computed(() => this.themeService.layoutThemeMode());
|
|
614
|
+
|
|
615
|
+
onLayoutChange(event: CustomEvent): void {
|
|
616
|
+
const layout = event.detail.value as LayoutMode;
|
|
617
|
+
this.themeService.setLayoutTheme(layout);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
```
|
|
621
|
+
|
|
622
|
+
#### Key Points
|
|
623
|
+
|
|
624
|
+
- **Independent Themes**: Color and layout themes work independently — combine light/dark with any layout mode
|
|
625
|
+
- **Signal-Based**: Use `computed()` to derive component state from service signals for zoneless compatibility
|
|
626
|
+
- **Default Has No Class**: The `default` layout uses the base theme variables (no class applied)
|
|
627
|
+
- **Consistent Variable Names**: Layout themes only override `--maki-*` spacing, typography, and radius variables
|
|
628
|
+
- **Accessibility Friendly**: `comfortable` mode improves readability for users who need larger text
|
|
629
|
+
|
|
630
|
+
For complete examples including both color and layout themes, see the [playground app](../../projects/playground/src/app/home/home.page.ts) source code.
|
|
631
|
+
|
|
632
|
+
## Components
|
|
633
|
+
|
|
634
|
+
### ButtonComponent
|
|
635
|
+
|
|
636
|
+
Ionic button with loading state, icon support, and dropdown functionality.
|
|
637
|
+
|
|
638
|
+
```typescript
|
|
639
|
+
import { ButtonComponent } from '@makigamestudio/ui-ionic';
|
|
640
|
+
|
|
641
|
+
// In your component
|
|
642
|
+
button: IonicActionButton = {
|
|
643
|
+
type: ActionButtonType.Button,
|
|
644
|
+
label: 'Save',
|
|
645
|
+
icon: 'save-outline',
|
|
646
|
+
color: 'primary',
|
|
647
|
+
fill: 'solid'
|
|
648
|
+
};
|
|
649
|
+
```
|
|
650
|
+
|
|
651
|
+
```html
|
|
652
|
+
<maki-button [button]="button" />
|
|
25
653
|
```
|
|
26
654
|
|
|
27
|
-
|
|
655
|
+
### ActionButtonListComponent
|
|
28
656
|
|
|
29
|
-
|
|
657
|
+
Popover list of action buttons.
|
|
658
|
+
|
|
659
|
+
```typescript
|
|
660
|
+
import { ActionButtonListComponent } from '@makigamestudio/ui-ionic';
|
|
661
|
+
|
|
662
|
+
buttons: IonicActionButton[] = [
|
|
663
|
+
{ type: ActionButtonType.Button, label: 'Edit', icon: 'create-outline' },
|
|
664
|
+
{ type: ActionButtonType.Button, label: 'Delete', icon: 'trash-outline', color: 'danger' }
|
|
665
|
+
];
|
|
666
|
+
```
|
|
30
667
|
|
|
31
|
-
|
|
668
|
+
```html
|
|
669
|
+
<maki-action-button-list [buttons]="buttons" />
|
|
670
|
+
```
|
|
32
671
|
|
|
33
|
-
|
|
672
|
+
### MakiTooltipDirective
|
|
34
673
|
|
|
35
|
-
|
|
36
|
-
cd dist/ui-ionic
|
|
37
|
-
```
|
|
674
|
+
Device-aware tooltips with Ionic color support.
|
|
38
675
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
```
|
|
676
|
+
```html
|
|
677
|
+
<button makiTooltip="Click to save" makiTooltipColor="primary">Save</button>
|
|
678
|
+
```
|
|
43
679
|
|
|
44
|
-
##
|
|
680
|
+
## Development
|
|
45
681
|
|
|
46
|
-
|
|
682
|
+
### Building the Library
|
|
47
683
|
|
|
48
684
|
```bash
|
|
49
|
-
|
|
685
|
+
npm run build:ionic
|
|
50
686
|
```
|
|
51
687
|
|
|
52
|
-
|
|
688
|
+
### Testing
|
|
689
|
+
|
|
690
|
+
```bash
|
|
691
|
+
npm run test:ionic
|
|
692
|
+
```
|
|
53
693
|
|
|
54
|
-
|
|
694
|
+
### Watch Mode
|
|
55
695
|
|
|
56
696
|
```bash
|
|
57
|
-
|
|
697
|
+
npm run watch:ionic
|
|
58
698
|
```
|
|
59
699
|
|
|
60
|
-
|
|
700
|
+
## License
|
|
701
|
+
|
|
702
|
+
MIT
|
|
61
703
|
|
|
62
|
-
##
|
|
704
|
+
## More Information
|
|
63
705
|
|
|
64
|
-
For
|
|
706
|
+
For detailed API documentation, examples, and architectural guidelines, see the [GitHub repository](https://github.com/gdor/ui-core).
|
|
707
|
+
````
|