@makigamestudio/ui-ionic
v0.9.1
Published
Ionic implementation of @makigamestudio/ui-core. Provides Ionic-specific button components with signal-based state management.
Downloads
529
Maintainers
Readme
@makigamestudio/ui-ionic
Ionic 8 component implementations for the ui-core library. This package provides ready-to-use Angular 21 standalone components built with Ionic Framework.
Features
- Zoneless Change Detection — No zone.js dependency
- Signal-Based Architecture — Angular Signals for all reactivity
- Standalone Components — No NgModules required
- Ionic 8 Integration — Native Ionic components and styling
- TypeScript Strict Mode — Full type safety
Installation
npm install @makigamestudio/ui-ionic @makigamestudio/ui-corePeer Dependencies
{
"@angular/core": "^21.0.0",
"@ionic/angular": "^8.0.0",
"@makigamestudio/ui-core": "^0.5.0"
}Theming
Quick Start
Import the theme file in your global styles to get both CSS variables and component styles:
// src/global.scss or src/styles.scss
@use '@makigamestudio/ui-ionic/theme.scss';
// Your custom overrides AFTER the import
:root {
--maki-spacing-md: 16px; // Override default spacing
}Note: Importing theme.scss automatically includes global-styles.scss, so you only need one import.
Theming Strategy
This library follows a hybrid approach for maximum flexibility:
Uses Ionic's color system — All components use
--ion-color-*variables (primary, secondary, success, etc.) so they automatically match your app's Ionic theme.Defines custom design tokens — Only for properties that Ionic doesn't provide: spacing, border radius, shadows, and typography.
This means when you change your Ionic theme colors, all ui-ionic components automatically adapt. No duplicate configuration needed!
Import Order
Always import the theme before your custom overrides:
// ✅ CORRECT ORDER
@use '@makigamestudio/ui-ionic/theme.scss';
:root {
--maki-spacing-lg: 20px;
}
// ❌ WRONG ORDER (overrides won't work)
:root {
--maki-spacing-lg: 20px;
}
@use '@makigamestudio/ui-ionic/theme.scss'; // This resets your overrides!Available CSS Variables
All variables include fallback values for robustness.
Spacing Scale
--maki-spacing-xs: 4px;
--maki-spacing-sm: 8px;
--maki-spacing-md: 12px;
--maki-spacing-lg: 16px;
--maki-spacing-xl: 24px;
--maki-spacing-xxl: 32px;Border Radius Scale
--maki-radius-xs: 4px;
--maki-radius-sm: 8px;
--maki-radius-md: 12px;
--maki-radius-lg: 16px;
--maki-radius-xl: 24px;
--maki-radius-full: 9999px;Shadows
--maki-box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
/* Dark mode: 0 2px 8px rgba(0, 0, 0, 0.3) */Typography - Font Sizes
--maki-font-size-xs: 0.75rem;
--maki-font-size-sm: 0.875rem;
--maki-font-size-md: 1rem;
--maki-font-size-lg: 1.125rem;
--maki-font-size-xl: 1.25rem;
--maki-font-size-xxl: 1.5rem;
--maki-font-size-xxxl: 2rem;Typography - Line Heights
--maki-line-height-tight: 1.25;
--maki-line-height-normal: 1.5;
--maki-line-height-relaxed: 1.75;Typography - Letter Spacing
--maki-letter-spacing-normal: 0;
--maki-letter-spacing-wide: 0.02em;
**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.
**Z-Index Hierarchy:**
```
Modal (10000) ← Highest layer (blocks everything)
├─ Tooltip (9999) ← Above popovers, below modals
├─ Popover (9000) ← Dropdowns and context menus
├─ Sticky (1020) ← Sticky headers
└─ Dropdown (1000) ← Basic dropdowns
```
This hierarchy ensures tooltips appear above popovers/dropdowns but below modal backdrops.
### Customization Examples
#### Override Spacing
```scss
@use '@makigamestudio/ui-ionic/theme.scss';
:root {
--maki-spacing-md: 16px; // Change medium spacing
--maki-spacing-lg: 24px; // Change large spacing
}
```
#### Change Shadows
```scss
@use '@makigamestudio/ui-ionic/theme.scss';
:root {
--maki-box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); // Stronger shadow
}
```
#### Customize Border Radius
```scss
@use '@makigamestudio/ui-ionic/theme.scss';
:root {
--maki-radius-sm: 12px; // Rounder tooltips and cards
}
```
### Shadow DOM and RTL Support
The library properly handles Ionic's Shadow DOM components and provides RTL (Right-to-Left) language support through CSS custom properties.
#### Styling Ionic Shadow DOM Components
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:
```typescript
// button.component.ts
ion-button {
--padding-start: var(--maki-spacing-sm, 0.5rem); // Maps to Shadow DOM
--padding-end: var(--maki-spacing-sm, 0.5rem);
}
```
**Why this pattern is correct:**
1. **Shadow DOM Encapsulation**: Regular `padding` CSS won't work on `ion-button` — you must use Ionic's custom properties
2. **Consistent Theming**: All spacing uses `--maki-*` tokens, ensuring layout themes affect Ionic components
3. **RTL Support**: `--padding-start`/`--padding-end` automatically flip in RTL languages (unlike `padding-left`/`padding-right`)
**RTL Behavior:**
```
LTR (English): --padding-start → left, --padding-end → right
RTL (Arabic): --padding-start → right, --padding-end → left
```
### Theming Architecture
The library follows a **hybrid theming approach** that cleanly separates UI-agnostic logic from CSS-specific implementation:
#### Separation of Concerns
**ui-core (UI-Agnostic):**
- ✅ Pure computational logic (position calculations, visibility rules)
- ✅ State management with signals
- ✅ Behavioral timing (show/close delays remain hardcoded: 250ms/200ms)
- ❌ No CSS, no DOM manipulation, no framework-specific code
**ui-ionic (Ionic-Specific):**
- ✅ CSS variables and theming
- ✅ DOM manipulation and rendering
- ✅ Ionic component integration
- ✅ CSS transitions and animations
#### Timing Strategy
The library uses a **hybrid timing approach**:
**Behavioral Delays (Service-Controlled):**
```typescript
// ui-core/tooltip.service.ts - Hardcoded for consistent UX
getShowDelayMs(): number { return 250; } // Wait before showing tooltip
getCloseDelayMs(): number { return 200; } // Grace period before hiding
```
These values are intentionally hardcoded to prevent consumers from setting extreme values (e.g., 5000ms) that would break expected tooltip behavior.
**CSS Timing (Theme-Controlled):**
```scss
// ui-ionic/theme.scss - Themable for visual preferences
```
These values can be customized per layout theme (compact uses faster transitions, comfortable uses slower).
**Why This Separation:**
- **Behavioral delays** define UX patterns (how long to wait before showing)
- **CSS timing** defines visual polish (how fast animations feel)
- Mixing these concerns would make layout themes accidentally break tooltip interaction patterns
### Dark Mode Toggle
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.
#### 1. Create a Theme Toggle Service
Create a signal-based service to manage theme state:
```typescript
// services/theme-toggle.service.ts
import { effect, inject, Injectable, Renderer2, RendererFactory2, signal } from '@angular/core';
import { DOCUMENT } from '@angular/common';
export type ThemeMode = 'light' | 'dark';
@Injectable({ providedIn: 'root' })
export class ThemeToggleService {
private readonly document = inject(DOCUMENT);
private readonly renderer: Renderer2;
private readonly storageKey = 'theme-mode';
private readonly _colorThemeMode = signal<ThemeMode>(this.getInitialColorTheme());
readonly mode = this._colorThemeMode.asReadonly();
constructor() {
const rendererFactory = inject(RendererFactory2);
this.renderer = rendererFactory.createRenderer(null, null);
// Apply initial theme class
this.applyThemeClass(this._colorThemeMode());
// Batch localStorage writes using effect
effect(() => {
const currentMode = this._colorThemeMode();
try {
localStorage.setItem(this.storageKey, currentMode);
} catch (error) {
console.error('Failed to persist theme:', error);
}
});
}
toggleColorTheme(): void {
this.setColorTheme(this._colorThemeMode() === 'light' ? 'dark' : 'light');
}
setColorTheme(mode: ThemeMode): void {
this._colorThemeMode.set(mode);
this.applyThemeClass(mode);
}
private getInitialColorTheme(): ThemeMode {
try {
const stored = localStorage.getItem(this.storageKey);
if (stored === 'light' || stored === 'dark') return stored;
} catch {}
// Fallback to prefers-color-scheme
if (this.document.defaultView?.matchMedia?.('(prefers-color-scheme: dark)').matches) {
return 'dark';
}
return 'light';
}
private applyThemeClass(mode: ThemeMode): void {
const body = this.document.body;
if (!body) return;
if (mode === 'dark') {
this.renderer.addClass(body, 'dark');
} else {
this.renderer.removeClass(body, 'dark');
}
}
}
```
#### 2. Define Dark Theme Variables
Create a dark theme stylesheet with Ionic's recommended dark palette:
```scss
// theme-dark.scss
body.dark {
// Ionic Core Colors (Dark Mode)
--ion-background-color: #121212;
--ion-text-color: #ffffff;
// Primary - Lighter for better contrast on dark backgrounds
--ion-color-primary: #428cff;
--ion-color-primary-contrast: #ffffff;
// Secondary
--ion-color-secondary: #50c8ff;
--ion-color-secondary-contrast: #ffffff;
// Success
--ion-color-success: #2fdf75;
--ion-color-success-contrast: #000000;
// Warning
--ion-color-warning: #ffd534;
--ion-color-warning-contrast: #000000;
// Danger
--ion-color-danger: #ff4961;
--ion-color-danger-contrast: #ffffff;
// Light - Inverted (becomes darker)
--ion-color-light: #222428;
--ion-color-light-contrast: #ffffff;
// Medium
--ion-color-medium: #989aa2;
--ion-color-medium-contrast: #000000;
// Dark - Inverted (becomes lighter)
--ion-color-dark: #f4f5f8;
--ion-color-dark-contrast: #000000;
// Maki Design Tokens
--maki-box-shadow: 0 2px 8px rgba(0, 0, 0, 0.5);
}
```
#### 3. Import Dark Theme
Add the dark theme import to your global styles:
```scss
// styles.scss
@use '@makigamestudio/ui-ionic/theme.scss';
@use './theme-dark.scss'; // Your dark theme overrides
```
#### 4. Add Theme Toggle UI
Use the theme service in your components:
```typescript
import { computed, inject } from '@angular/core';
import { IonToggle } from '@ionic/angular/standalone';
import { ThemeToggleService } from './services/theme-toggle.service';
@Component({
imports: [IonToggle],
template: `
<ion-toggle
[checked]="isDark()"
(ionChange)="onThemeToggle($event)"
aria-label="Toggle dark mode"
/>
`
})
export class MyComponent {
readonly themeService = inject(ThemeToggleService);
readonly isDark = computed(() => this.themeService.mode() === 'dark');
onThemeToggle(event: CustomEvent): void {
this.themeService.setColorTheme(event.detail.checked ? 'dark' : 'light');
}
}
```
#### Key Points
- **CSS Variables Required**: Always use CSS variables in your components (e.g., `var(--ion-color-primary)`) so they adapt automatically to theme changes
- **Class-Based Theming**: The service applies/removes the `dark` class on `<body>` to trigger theme overrides
- **Persistence**: Theme preference is saved to `localStorage` and restored on app load
- **Fallback Support**: Automatically detects `prefers-color-scheme: dark` if no saved preference exists
- **Signal-Based**: Fully reactive using Angular signals for zoneless compatibility
For a complete working example, see the [playground app](../../projects/playground/src/app/home/home.page.ts) source code.
### Layout Theme Toggle
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**.
#### Layout Theme Comparison
| Property | Compact | Default (Baseline) | Comfortable |
| ------------------ | ------- | ------------------ | ----------- |
| **Line Height** | | | |
| Tight | 1.2 | 1.25 | 1.4 |
| Normal | 1.3 | 1.5 | 1.75 |
| Relaxed | 1.5 | 1.75 | 2.0 |
| **Letter Spacing** | | | |
| Normal | 0 | 0 | 0 |
| Wide | 0.02em | 0.02em | 0.04em |
#### Layout Theme Options
- **Compact**: Reduced spacing (15-20% smaller), tighter line heights — ideal for information-dense interfaces or large screens
- **Default**: Standard spacing and typography (library baseline) — balanced for most use cases
- **Comfortable**: Increased spacing (20-25% larger), relaxed line heights, wider letter spacing — better accessibility and readability
#### 1. Create Layout Theme Stylesheets
Create separate SCSS files for each layout variant:
```scss
// theme-layout-compact.scss
body.layout-compact {
// Reduced spacing (20% smaller)
--maki-spacing-xxs: 0.1rem;
--maki-spacing-xs: 0.2rem;
--maki-spacing-sm: 0.4rem;
--maki-spacing-md: 0.6rem;
--maki-spacing-lg: 0.8rem;
--maki-spacing-xl: 1.2rem;
--maki-spacing-xxl: 1.6rem;
// Tighter typography
--maki-font-size-xs: 0.6875rem;
--maki-font-size-sm: 0.8125rem;
--maki-font-size-md: 0.9375rem;
// ... other font sizes
// Smaller border radius
--maki-radius-xs: 0.2rem;
--maki-radius-sm: 0.375rem;
// ... other radius values
}
// theme-layout-comfortable.scss
body.layout-comfortable {
// Increased spacing (25% larger)
--maki-spacing-xxs: 0.15625rem;
--maki-spacing-xs: 0.3125rem;
--maki-spacing-sm: 0.625rem;
--maki-spacing-md: 0.9375rem;
--maki-spacing-lg: 1.25rem;
--maki-spacing-xl: 1.875rem;
--maki-spacing-xxl: 2.5rem;
// Larger typography for readability
--maki-font-size-xs: 0.8125rem;
--maki-font-size-sm: 0.9375rem;
--maki-font-size-md: 1.0625rem;
// ... other font sizes
// Larger border radius
--maki-radius-xs: 0.3125rem;
--maki-radius-sm: 0.625rem;
// ... other radius values
}
```
#### 2. Update Theme Service
Extend your `ThemeToggleService` to manage layout modes:
```typescript
export type LayoutMode = 'compact' | 'default' | 'comfortable';
@Injectable({ providedIn: 'root' })
export class ThemeToggleService {
private readonly layoutStorageKey = 'layout-mode';
private readonly _layoutThemeMode = signal<LayoutMode>(this.getInitialLayoutTheme());
readonly layoutThemeMode = this._layoutThemeMode.asReadonly();
constructor() {
// ... existing color theme setup
// Apply initial layout class
this.applyLayoutClass(this._layoutThemeMode());
// Batch layout localStorage writes
effect(() => {
const currentLayoutMode = this._layoutThemeMode();
try {
localStorage.setItem(this.layoutStorageKey, currentLayoutMode);
} catch (error) {
console.error('Failed to persist layout theme:', error);
}
});
}
setLayoutTheme(mode: LayoutMode): void {
this._layoutThemeMode.set(mode);
this.applyLayoutClass(mode);
}
private getInitialLayoutTheme(): LayoutMode {
try {
const stored = localStorage.getItem(this.layoutStorageKey);
if (stored === 'compact' || stored === 'default' || stored === 'comfortable') {
return stored;
}
} catch {}
return 'default';
}
private applyLayoutClass(mode: LayoutMode): void {
const body = this.document.body;
if (!body) return;
// Remove all layout classes
this.renderer.removeClass(body, 'layout-compact');
this.renderer.removeClass(body, 'layout-comfortable');
// Apply appropriate class (default has no class)
if (mode === 'compact') {
this.renderer.addClass(body, 'layout-compact');
} else if (mode === 'comfortable') {
this.renderer.addClass(body, 'layout-comfortable');
}
}
}
```
#### 3. Import Layout Themes
Add layout theme imports to your global styles:
```scss
// styles.scss
@use '@makigamestudio/ui-ionic/theme.scss';
@use './theme-dark.scss';
@use './theme-layout-compact.scss';
@use './theme-layout-comfortable.scss';
```
#### 4. Add Layout Switcher UI
Use `ion-segment` for layout mode selection:
```typescript
import { computed, inject } from '@angular/core';
import { IonSegment, IonSegmentButton, IonLabel } from '@ionic/angular/standalone';
@Component({
imports: [IonSegment, IonSegmentButton, IonLabel],
template: `
<ion-segment [value]="layoutThemeMode()" (ionChange)="onLayoutChange($event)">
<ion-segment-button value="compact">
<ion-label>Compact</ion-label>
</ion-segment-button>
<ion-segment-button value="default">
<ion-label>Default</ion-label>
</ion-segment-button>
<ion-segment-button value="comfortable">
<ion-label>Comfortable</ion-label>
</ion-segment-button>
</ion-segment>
`
})
export class MyComponent {
readonly themeService = inject(ThemeToggleService);
readonly layoutThemeMode = computed(() => this.themeService.layoutThemeMode());
onLayoutChange(event: CustomEvent): void {
const layout = event.detail.value as LayoutMode;
this.themeService.setLayoutTheme(layout);
}
}
```
#### Key Points
- **Independent Themes**: Color and layout themes work independently — combine light/dark with any layout mode
- **Signal-Based**: Use `computed()` to derive component state from service signals for zoneless compatibility
- **Default Has No Class**: The `default` layout uses the base theme variables (no class applied)
- **Consistent Variable Names**: Layout themes only override `--maki-*` spacing, typography, and radius variables
- **Accessibility Friendly**: `comfortable` mode improves readability for users who need larger text
For complete examples including both color and layout themes, see the [playground app](../../projects/playground/src/app/home/home.page.ts) source code.
## Components
### ButtonComponent
Ionic button with loading state, icon support, and dropdown functionality.
```typescript
import { ButtonComponent } from '@makigamestudio/ui-ionic';
// In your component
button: IonicActionButton = {
type: ActionButtonType.Button,
label: 'Save',
icon: 'save-outline',
color: 'primary',
fill: 'solid'
};
```
```html
<maki-button [button]="button" />
```
### ActionButtonListComponent
Popover list of action buttons.
```typescript
import { ActionButtonListComponent } from '@makigamestudio/ui-ionic';
buttons: IonicActionButton[] = [
{ type: ActionButtonType.Button, label: 'Edit', icon: 'create-outline' },
{ type: ActionButtonType.Button, label: 'Delete', icon: 'trash-outline', color: 'danger' }
];
```
```html
<maki-action-button-list [buttons]="buttons" />
```
### MakiTooltipDirective
Device-aware tooltips with Ionic color support.
```html
<button makiTooltip="Click to save" makiTooltipColor="primary">Save</button>
```
## Development
### Building the Library
```bash
npm run build:ionic
```
### Testing
```bash
npm run test:ionic
```
### Watch Mode
```bash
npm run watch:ionic
```
## License
MIT
## More Information
For detailed API documentation, examples, and architectural guidelines, see the [GitHub repository](https://github.com/gdor/ui-core).