
Security News
Axios Supply Chain Attack Reaches OpenAI macOS Signing Pipeline, Forces Certificate Rotation
OpenAI rotated macOS signing certificates after a malicious Axios package reached its CI pipeline in a broader software supply chain attack.
column-fitter
Advanced tools
The column-fitter library provides a responsive grid layout system that automatically adjusts the number of columns based on the detected device size. It integrates with the screen-observer package to monitor device changes and dynamically updates CSS Grid layouts for optimal viewing across different screen sizes.
✅ Device Size Detection - Automatic detection via screen-observer integration
✅ Dynamic Grid Updates - Real-time column count adjustment
✅ CSS Grid Foundation - Modern CSS Grid with repeat() and auto-fit
✅ Flexible Configuration - Fixed numbers or device-specific settings
✅ Customizable Styling - Configurable gap, margins, padding, and colors
✅ Performance Optimized - Efficient change detection and updates
✅ Type-Safe Configuration - Strong typing with Column and DeviceSizes models
✅ Demo Component - Interactive demo showcasing all features
| Feature | Description |
|---|---|
| Automatic Responsiveness | No manual media queries needed |
| Device-Aware Layouts | Optimized layouts for each device type |
| Modern CSS Grid | Leverages CSS Grid for superior performance |
| Type-Safe Configuration | Full TypeScript support with device models |
| Seamless Integration | Works with existing screen-observer implementations |
ColumnFitterDemoComponent)The demo component showcases responsive grid layouts using a bookmarks list example.
To use the demo component in your application:
<app-column-fitter-demo></app-column-fitter-demo>
The column-fitter library provides a modern, responsive grid system that automatically adapts column layouts based on device detection, making it perfect for creating responsive applications without manual media query management.
// app.module.ts
import { ColumnFitterModule } from 'column-fitter';
@NgModule({
imports: [
ColumnFitterModule
]
})
export class AppModule { }
The package requires the screen-observer package for device detection:
npm install screen-observer
import { Component } from '@angular/core';
@Component({
selector: 'app-fixed-grid',
template: `
<app-column-fitter
[columns]="4"
[gap]="'1rem'"
[padding]="'1rem'"
[backgroundColor]="'#f5f5f5'">
<div class="grid-item" *ngFor="let item of items">
{{ item.name }}
</div>
</app-column-fitter>
`
})
export class FixedGridComponent {
items = [
{ id: 1, name: 'Item 1' },
{ id: 2, name: 'Item 2' },
{ id: 3, name: 'Item 3' },
{ id: 4, name: 'Item 4' },
{ id: 5, name: 'Item 5' },
{ id: 6, name: 'Item 6' }
];
}
import { Component } from '@angular/core';
import { Column, DeviceSizes } from 'column-fitter';
@Component({
selector: 'app-responsive-grid',
template: `
<app-column-fitter
[columns]="responsiveColumns"
[gap]="'1.5rem'"
[margin]="'1rem'"
[minWidth]="'250px'"
[backgroundColor]="'#ffffff'"
[padding]="'1rem'">
<div class="product-card" *ngFor="let product of products">
<h3>{{ product.name }}</h3>
<p>{{ product.description }}</p>
<span class="price">{{ product.price | currency }}</span>
</div>
</app-column-fitter>
`,
styles: [`
.product-card {
padding: 1rem;
border: 1px solid #ddd;
border-radius: 8px;
background: white;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.price {
font-weight: bold;
color: #2196f3;
}
`]
})
export class ResponsiveGridComponent {
responsiveColumns: Column[] = [
{ device: DeviceSizes.MOBILE, columns: 1 },
{ device: DeviceSizes.MINI, columns: 2 },
{ device: DeviceSizes.TABLET, columns: 3 },
{ device: DeviceSizes.DESKTOP, columns: 4 }
];
products = [
{ id: 1, name: 'Product 1', description: 'Description 1', price: 29.99 },
{ id: 2, name: 'Product 2', description: 'Description 2', price: 39.99 },
{ id: 3, name: 'Product 3', description: 'Description 3', price: 49.99 },
{ id: 4, name: 'Product 4', description: 'Description 4', price: 59.99 },
{ id: 5, name: 'Product 5', description: 'Description 5', price: 69.99 },
{ id: 6, name: 'Product 6', description: 'Description 6', price: 79.99 }
];
}
import { Component } from '@angular/core';
import { Column, DeviceSizes } from 'column-fitter';
@Component({
selector: 'app-image-gallery',
template: `
<app-column-fitter
[columns]="galleryColumns"
[gap]="'0.5rem'"
[padding]="'0.5rem'"
[backgroundColor]="'#000'"
[minWidth]="'200px'">
<div class="gallery-item" *ngFor="let image of images">
<img [src]="image.url" [alt]="image.alt" />
<div class="overlay">
<h4>{{ image.title }}</h4>
</div>
</div>
</app-column-fitter>
`,
styles: [`
.gallery-item {
position: relative;
overflow: hidden;
border-radius: 4px;
aspect-ratio: 1;
}
.gallery-item img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.3s ease;
}
.gallery-item:hover img {
transform: scale(1.1);
}
.overlay {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: linear-gradient(transparent, rgba(0,0,0,0.8));
color: white;
padding: 1rem;
transform: translateY(100%);
transition: transform 0.3s ease;
}
.gallery-item:hover .overlay {
transform: translateY(0);
}
`]
})
export class ImageGalleryComponent {
galleryColumns: Column[] = [
{ device: DeviceSizes.MOBILE, columns: 2 },
{ device: DeviceSizes.MINI, columns: 3 },
{ device: DeviceSizes.TABLET, columns: 4 },
{ device: DeviceSizes.DESKTOP, columns: 6 }
];
images = [
{ id: 1, url: 'https://picsum.photos/300/300?random=1', alt: 'Random 1', title: 'Image 1' },
{ id: 2, url: 'https://picsum.photos/300/300?random=2', alt: 'Random 2', title: 'Image 2' },
{ id: 3, url: 'https://picsum.photos/300/300?random=3', alt: 'Random 3', title: 'Image 3' },
{ id: 4, url: 'https://picsum.photos/300/300?random=4', alt: 'Random 4', title: 'Image 4' },
{ id: 5, url: 'https://picsum.photos/300/300?random=5', alt: 'Random 5', title: 'Image 5' },
{ id: 6, url: 'https://picsum.photos/300/300?random=6', alt: 'Random 6', title: 'Image 6' }
];
}
import { Component } from '@angular/core';
import { Column, DeviceSizes } from 'column-fitter';
@Component({
selector: 'app-dashboard',
template: `
<div class="dashboard-container">
<h2>Dashboard</h2>
<app-column-fitter
[columns]="dashboardColumns"
[gap]="'1rem'"
[padding'"
[background]="'1remColor]="'#f8f9fa'"
[minWidth]="'300px'">
<div class="stat-card" *ngFor="let stat of statistics">
<div class="stat-icon">
<mat-icon>{{ stat.icon }}</mat-icon>
</div>
<div class="stat-content">
<h3>{{ stat.value }}</h3>
<p>{{ stat.label }}</p>
</div>
</div>
</app-column-fitter>
</div>
`,
styles: [`
.dashboard-container {
padding: 2rem;
}
.dashboard-container h2 {
margin-bottom: 2rem;
color: #333;
}
.stat-card {
background: white;
padding: 1.5rem;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
display: flex;
align-items: center;
gap: 1rem;
}
.stat-icon {
width: 48px;
height: 48px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
background: #e3f2fd;
color: #1976d2;
}
.stat-content h3 {
margin: 0;
font-size: 1.5rem;
font-weight: bold;
color: #333;
}
.stat-content p {
margin: 0;
color: #666;
font-size: 0.9rem;
}
`]
})
export class DashboardComponent {
dashboardColumns: Column[] = [
{ device: DeviceSizes.MOBILE, columns: 1 },
{ device: DeviceSizes.TABLET, columns: 2 },
{ device: DeviceSizes.MINI, columns: 2 },
{ device: DeviceSizes.DESKTOP, columns: 4 }
];
statistics = [
{ id: 1, icon: 'people', value: '1,234', label: 'Total Users' },
{ id: 2, icon: 'shopping_cart', value: '$12,345', label: 'Revenue' },
{ id: 3, icon: 'trending_up', value: '98.5%', label: 'Growth Rate' },
{ id: 4, icon: 'assignment', value: '567', label: 'Tasks Completed' },
{ id: 5, icon: 'star', value: '4.8/5', label: 'Customer Rating' },
{ id: 6, icon: 'notifications', value: '23', label: 'Pending Alerts' }
];
}
| Input | Type | Description | Default |
|---|---|---|---|
padding | string | Padding for the grid container (CSS padding value) | '' |
margin | string | Margin for the grid container (CSS margin value) | '' |
backgroundColor | string | Background color for the grid container | '' |
minWidth | string | Minimum width for auto-fit columns (CSS length value) | '' |
gap | string | Gap between grid items (CSS gap value) | '1rem' |
columns | number | Column[] | Column configuration - fixed number or device-specific array | 0 |
| Property | Type | Description |
|---|---|---|
gridColumns | string | Current CSS grid-template-columns value |
hasColumns | boolean | Whether valid column configuration exists |
subscriptions | Subscription | RxJS subscription management |
export enum DeviceSizes {
DESKTOP = 'desktop', // Desktop/large screens
TABLET = 'tablet', // Tablet devices
MINI = 'mini', // Small tablets/large phones
MOBILE = 'mobile' // Mobile phones
}
export interface ColumnInterface {
device: DeviceSizes; // Target device type
columns: number; // Number of columns for this device
}
export class Column implements ColumnInterface {
constructor(
public device = DeviceSizes.DESKTOP,
public columns = 0,
) {}
static adapt(item?: any): Column {
return new Column(
item?.device,
item?.columns
);
}
}
// Device-specific column configurations
const responsiveColumns: Column[] = [
new Column(DeviceSizes.MOBILE, 1), // 1 column on mobile
new Column(DeviceSizes.MINI, 2), // 2 columns on mini devices
new Column(DeviceSizes.TABLET, 3), // 3 columns on tablets
new Column(DeviceSizes.DESKTOP, 4) // 4 columns on desktop
];
// Using adapt method
const adaptedColumns = [
Column.adapt({ device: DeviceSizes.MOBILE, columns: 1 }),
Column.adapt({ device: DeviceSizes.TABLET, columns: 3 })
];
// Mixed configuration
const mixedColumns: (number | Column[])[] = [
3, // Fixed 3 columns for all devices
// OR
[
{ device: DeviceSizes.MOBILE, columns: 1 },
{ device: DeviceSizes.TABLET, columns: 2 },
{ device: DeviceSizes.DESKTOP, columns: 4 }
]
];
The component automatically generates CSS Grid templates based on the configuration:
// Input: columns = 3
// Output: gridColumns = 'repeat(3, 1fr)'
// Input: columns = 0 (disabled)
// Output: gridColumns = 'repeat(auto-fit, minmax(250px, 1fr))'
// Device detection logic
if (device === 'desktop' && found(DeviceSizes.DESKTOP)) {
const cols = found(DeviceSizes.DESKTOP) as Column;
return `repeat(${cols.columns}, 1fr)`;
}
// Fallback for unmatched devices
return `repeat(auto-fit, minmax(${this.minWidth}, 1fr))`;
screenObserverService.device$distinctUntilChanged() to prevent unnecessary updatesgetGridTemplateColumns() with current devicegridColumns property with new CSS Grid valueNo Global Configuration Required
The ColumnFitterModule does not provide a forRoot() method or global configuration options. All configuration is done at the component level through input properties.
@NgModule({
declarations: [
ColumnFitterComponent,
ColumnFitterDemoComponent
],
imports: [
// Dependencies are imported by the consuming application
// screen-observer must be installed separately
],
exports: [
ColumnFitterComponent,
ColumnFitterDemoComponent
]
})
export class ColumnFitterModule { }
The component uses CSS Grid with the following base styles:
:host {
display: block;
}
.grid-container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1rem;
}
// Enhanced grid styling
:host ::ng-deep .grid-container {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 12px;
padding: 2rem;
.grid-item {
background: white;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
transition: transform 0.2s ease, box-shadow 0.2s ease;
&:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
}
}
}
// Dynamic gaps based on device
:host ::ng-deep .grid-container {
gap: var(--grid-gap, 1rem);
@media (max-width: 768px) {
--grid-gap: 0.5rem;
}
@media (min-width: 1200px) {
--grid-gap: 1.5rem;
}
}
// CSS Grid with masonry-like behavior
.masonry-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
grid-auto-rows: 200px;
gap: 1rem;
.masonry-item {
grid-row-end: span var(--row-span, 1);
&.large {
--row-span: 2;
}
&.wide {
grid-column-end: span 2;
}
}
}
// Card layout with consistent height
.card-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 1.5rem;
align-items: stretch;
.card {
display: flex;
flex-direction: column;
height: 100%;
background: white;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
.card-image {
height: 200px;
overflow: hidden;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
.card-content {
padding: 1.5rem;
flex: 1;
display: flex;
flex-direction: column;
}
}
}
import { Component } from '@angular/core';
import { Column, DeviceSizes } from 'column-fitter';
@Component({
selector: 'app-material-grid',
template: `
<app-column-fitter
[columns]="materialColumns"
[gap]="'1rem'"
[padding]="'1rem'">
<mat-card class="grid-card" *ngFor="let item of materialItems">
<mat-card-header>
<mat-card-title>{{ item.title }}</mat-card-title>
<mat-card-subtitle>{{ item.subtitle }}</mat-card-subtitle>
</mat-card-header>
<img mat-card-image [src]="item.image" [alt]="item.title">
<mat-card-content>
<p>{{ item.description }}</p>
</mat-card-content>
<mat-card-actions>
<button mat-button>LIKE</button>
<button mat-button>SHARE</button>
</mat-card-actions>
</mat-card>
</app-column-fitter>
`
})
export class MaterialGridComponent {
materialColumns: Column[] = [
{ device: DeviceSizes.MOBILE, columns: 1 },
{ device: DeviceSizes.TABLET, columns: 2 },
{ device: DeviceSizes.DESKTOP, columns: 3 }
];
materialItems = [
{
title: 'Card 1',
subtitle: 'Subtitle 1',
description: 'Description for card 1',
image: 'https://picsum.photos/400/200?random=1'
},
// ... more items
];
}
import { Component } from '@angular/core';
import { Column, DeviceSizes } from 'column-fitter';
@Component({
selector: 'app-dynamic-content',
template: `
<div class="controls">
<button (click)="addItem()">Add Item</button>
<button (click)="removeItem()">Remove Item</button>
<select [(ngModel)]="selectedLayout" (change)="changeLayout()">
<option value="mobile1">Mobile: 1 Col</option>
<option value="tablet3">Tablet: 3 Col</option>
<option value="desktop4">Desktop: 4 Col</option>
</select>
</div>
<app-column-fitter
[columns]="currentColumns"
[gap]="'1rem'"
[padding]="'1rem'">
<div class="dynamic-item" *ngFor="let item of dynamicItems; trackBy: trackById">
<h3>{{ item.title }}</h3>
<p>{{ item.content }}</p>
<small>ID: {{ item.id }}</small>
</div>
</app-column-fitter>
`
})
export class DynamicContentComponent {
currentColumns: Column[] = [
{ device: DeviceSizes.MOBILE, columns: 1 },
{ device: DeviceSizes.TABLET, columns: 3 },
{ device: DeviceSizes.DESKTOP, columns: 4 }
];
selectedLayout = 'tablet3';
dynamicItems = [
{ id: 1, title: 'Item 1', content: 'Content 1' },
{ id: 2, title: 'Item 2', content: 'Content 2' },
{ id: 3, title: 'Item 3', content: 'Content 3' }
];
addItem() {
const newItem = {
id: Date.now(),
title: `Item ${this.dynamicItems.length + 1}`,
content: `Content ${this.dynamicItems.length + 1}`
};
this.dynamicItems = [...this.dynamicItems, newItem];
}
removeItem() {
if (this.dynamicItems.length > 0) {
this.dynamicItems = this.dynamicItems.slice(0, -1);
}
}
changeLayout() {
switch (this.selectedLayout) {
case 'mobile1':
this.currentColumns = [
{ device: DeviceSizes.MOBILE, columns: 1 },
{ device: DeviceSizes.TABLET, columns: 1 },
{ device: DeviceSizes.DESKTOP, columns: 1 }
];
break;
case 'tablet3':
this.currentColumns = [
{ device: DeviceSizes.MOBILE, columns: 1 },
{ device: DeviceSizes.TABLET, columns: 3 },
{ device: DeviceSizes.DESKTOP, columns: 3 }
];
break;
case 'desktop4':
this.currentColumns = [
{ device: DeviceSizes.MOBILE, columns: 1 },
{ device: DeviceSizes.TABLET, columns: 2 },
{ device: DeviceSizes.DESKTOP, columns: 4 }
];
break;
}
}
trackById(index: number, item: any): any {
return item.id;
}
}
The component uses several performance optimizations:
ngOnDestroy() {
// Clean up subscriptions to prevent memory leaks
this.subscriptions.unsubscribe();
}
// Use trackBy for large lists
@Component({
template: `
<app-column-fitter [columns]="columns">
<div *ngFor="let item of largeDataset; trackBy: trackById">
{{ item.name }}
</div>
</app-column-fitter>
`
})
export class LargeDatasetComponent {
trackById(index: number, item: any): any {
return item.id; // Use unique identifier
}
}
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ColumnFitterComponent } from './column-fitter.component';
import { Column, DeviceSizes } from './models/column.model';
import { ScreenObserverService } from 'screen-observer';
describe('ColumnFitterComponent', () => {
let component: ColumnFitterComponent;
let fixture: ComponentFixture<ColumnFitterComponent>;
let mockScreenObserverService: jasmine.SpyObj<ScreenObserverService>;
beforeEach(async () => {
const screenObserverSpy = jasmine.createSpyObj('ScreenObserverService', ['device$']);
await TestBed.configureTestingModule({
declarations: [ ColumnFitterComponent ],
providers: [
{ provide: ScreenObserverService, useValue: screenObserverSpy }
]
}).compileComponents();
fixture = TestBed.createComponent(ColumnFitterComponent);
component = fixture.componentInstance;
mockScreenObserverService = TestBed.inject(ScreenObserverService) as jasmine.SpyObj<ScreenObserverService>;
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should set fixed columns correctly', () => {
component.columns = 3;
fixture.detectChanges();
const result = component.getGridTemplateColumns('desktop');
expect(result).toBe('repeat(3, 1fr)');
});
it('should handle device-specific columns', () => {
component.columns = [
{ device: DeviceSizes.MOBILE, columns: 1 },
{ device: DeviceSizes.TABLET, columns: 3 },
{ device: DeviceSizes.DESKTOP, columns: 4 }
];
fixture.detectChanges();
expect(component.getGridTemplateColumns('mobile')).toBe('repeat(1, 1fr)');
expect(component.getGridTemplateColumns('tablet')).toBe('repeat(3, 1fr)');
expect(component.getGridTemplateColumns('desktop')).toBe('repeat(4, 1fr)');
});
it('should fallback to auto-fit when no matching device', () => {
component.columns = [
{ device: DeviceSizes.MOBILE, columns: 1 }
];
component.minWidth = '200px';
fixture.detectChanges();
const result = component.getGridTemplateColumns('desktop');
expect(result).toBe('repeat(auto-fit, minmax(200px, 1fr))');
});
it('should handle disabled state (columns = 0)', () => {
component.columns = 0;
component.minWidth = '250px';
fixture.detectChanges();
const result = component.getGridTemplateColumns('desktop');
expect(result).toBe('repeat(auto-fit, minmax(250px, 1fr))');
});
});
@Component({
template: `
<div class="debug-info">
Current Device: {{ currentDevice }}<br>
Grid Columns: {{ gridColumns }}<br>
Has Columns: {{ hasColumns }}<br>
Columns Config: {{ columns | json }}
</div>
<app-column-fitter
[columns]="columns"
[gap]="gap"
[minWidth]="minWidth">
<!-- Content -->
</app-column-fitter>
`
})
export class DebugColumnFitterComponent {
currentDevice = '';
gridColumns = '';
hasColumns = false;
columns: any = [];
gap = '1rem';
minWidth = '250px';
constructor() {
// Add debugging logic
}
}
ngOnInit() {
const start = performance.now();
this.subscriptions.add(
this.screenObserverService.device$.subscribe((screen: string) => {
const updateStart = performance.now();
this.gridColumns = this.getGridTemplateColumns(screen);
const updateEnd = performance.now();
console.log(`Grid update took ${updateEnd - updateStart}ms for device: ${screen}`);
})
);
const initEnd = performance.now();
console.log(`ColumnFitter initialization took ${initEnd - start}ms`);
}
The component requires CSS Grid support, which is available in:
// CSS Grid fallback using Flexbox
.grid-container {
display: flex;
flex-wrap: wrap;
margin: -0.5rem;
.grid-item {
flex: 1 1 250px; /* Minimum width of 250px */
margin: 0.5rem;
@supports (display: grid) {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
margin: 0;
}
}
}
// JavaScript fallback for older browsers
if (!CSS.supports('display', 'grid')) {
// Apply Flexbox fallback
this.applyFlexboxFallback();
}
FAQs
This is an Angular Module containing Components/Services using Material
The npm package column-fitter receives a total of 0 weekly downloads. As such, column-fitter popularity was classified as not popular.
We found that column-fitter demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
OpenAI rotated macOS signing certificates after a malicious Axios package reached its CI pipeline in a broader software supply chain attack.

Security News
Open source is under attack because of how much value it creates. It has been the foundation of every major software innovation for the last three decades. This is not the time to walk away from it.

Security News
Socket CEO Feross Aboukhadijeh breaks down how North Korea hijacked Axios and what it means for the future of software supply chain security.