Compare commits

..

2 Commits

Author SHA1 Message Date
m
cf0fe586eb Add IKEA furniture data research report 2026-02-07 12:50:33 +01:00
m
53ee0fc1ec Add custom furniture creator form to catalog panel
Adds a "Create Custom Furniture" button at the bottom of the catalog
sidebar that expands into a form with fields for name, dimensions
(W/D/H in meters), color picker, and category selector. Submitting
creates a simple box-geometry catalog item and adds it to the
in-memory catalog for immediate placement into rooms.
2026-02-07 12:45:09 +01:00
3 changed files with 620 additions and 0 deletions

352
RESEARCH-ikea-data.md Normal file
View File

@@ -0,0 +1,352 @@
# IKEA Furniture Data Import — Research Report
**Task:** t-05dd7
**Date:** 2026-02-07
**Researcher:** researcher role
## Executive Summary
**There is no official public IKEA API for furniture data.** However, there are multiple viable paths to get IKEA product dimensions and 3D models into our room planner. The recommended approach is a **tiered strategy**: start with a curated hand-built catalog of common IKEA items using known dimensions, with optional GLB model loading for enhanced visuals later.
---
## 1. IKEA APIs (Official)
### IKEA Has No Public Developer API
IKEA does not offer a public REST/GraphQL API for developers. There is no developer portal, no API keys, no official documentation.
### Undocumented Internal Endpoints
IKEA's website uses internal APIs that have been partially reverse-engineered:
| Endpoint | Purpose | Format |
|----------|---------|--------|
| `sik.search.blue.cdtapps.com/{country}/{lang}/search-result-page?q={query}&size=24&types=PRODUCT` | Product search | JSON |
| `www.ikea.com/{country}/{lang}/products/{itemNo[5:]}/{itemNo}.json` | Product detail (PIP) | JSON |
| `api.ingka.ikea.com/cia/availabilities/{country}/{lang}?itemNos={itemNo}` | Stock/availability | JSON |
| `www.ikea.com/{region}/{country}/iows/catalog/availability/{itemNo}` | Legacy availability (deprecated Dec 2021) | XML |
| `web-api.ikea.com/{country}/{lang}/rotera/data/exists/{itemNo}/` | 3D model existence check | JSON |
| `web-api.ikea.com/{country}/{lang}/rotera/data/model/{itemNo}/` | 3D model metadata + CDN URL | JSON |
The **Rotera API** is most relevant for us — it returns a `modelUrl` field pointing to the actual GLB file on IKEA's CDN. This is what the Blender IKEA Browser add-on uses internally.
**IKEA Kreativ**: Their AI-powered room planner at `ikea.com/us/en/home-design/` uses AWS infrastructure (API Gateway at `ddh79d7xh5.execute-api.us-east-1.amazonaws.com`). No public API.
**Status**: All endpoints are undocumented, unsupported, and may break at any time. Rate limiting and anti-bot measures exist. The IOWS endpoints are deprecated.
**Sources**: https://sbmueller.github.io/posts/ikea/ | Postman workspace: https://www.postman.com/galactic-shuttle-566922/ikea/overview
### Archived Python Client
**vrslev/ikea-api-client** (GitHub, 1k+ stars) — Python client for IKEA internal APIs supporting cart, search, stock, item info, and 3D models. **Archived October 2024** — no longer maintained. MIT license.
- Supported: stock lookup, product search, item specs, 3D model retrieval (`RoteraItem` endpoint)
- Install: `pip install ikea-api[httpx]`
- **Not recommended** for new projects since archived
**Source**: https://github.com/vrslev/ikea-api-client
### npm: ikea-availability-checker (Active)
**Ephigenia/ikea-availability-checker** — Node.js package for checking IKEA stock across 40+ countries and 400+ stores. Actively maintained.
```bash
npm install ikea-availability-checker
# CLI: ikea-availability-checker stock --country us 30457903
```
Not directly useful for dimensions/models but proves Node.js access to IKEA endpoints is feasible.
**Source**: https://github.com/Ephigenia/ikea-availability-checker
---
## 2. Open Datasets with Dimensions
### Best: Hugging Face — IKEA US CommerceTXT (30,511 products)
- **Records**: 30,511 products across 632 categories
- **Fields**: Name, SKU, Price, Dimensions (Width/Height/Depth), Materials, Category, Images, URLs
- **Format**: CommerceTXT v1.0.1 (text-based, parseable) + Parquet (56.5 MB)
- **License**: CC0 1.0 (Public Domain) — fully usable
- **Date**: July 15, 2025
- **Dimensions**: Yes, in the `@SPECS` section (e.g., `Width: 12", Height: 16"`)
```python
from datasets import load_dataset
dataset = load_dataset("tsazan/ikea-us-commercetxt")
```
**Source**: https://huggingface.co/datasets/tsazan/ikea-us-commercetxt
### Good: Kaggle — IKEA SA Furniture (2,962 items)
- **Records**: 2,962 items
- **Fields**: name, category, price, width (cm), height (cm), depth (cm), designer, description
- **Format**: CSV
- **License**: Kaggle dataset terms
- **Date**: April 2020 (somewhat dated)
- **Dimensions**: Yes — width, height, depth in centimeters
**Source**: https://www.kaggle.com/datasets/ahmedkallam/ikea-sa-furniture-web-scraping
### Good: GitHub — IKEA Dataset (12,600+ items)
- **Records**: 12,600+ images with furniture dimensions
- **Fields**: Name, dimensions (width/length/height cm), material
- **Format**: ZIP archives + Python scraping script
- **Organization**: By room (bedroom, bathroom, kitchen, living room, etc.)
- **Date**: January 2020
**Source**: https://github.com/valexande/IKEA-Dataset
### Reference: Dimensions.com — IKEA Collection
- **Content**: Dozens of IKEA products with precise dimensions + technical drawings
- **Formats**: DWG, SVG, JPG (2D) + 3DM, OBJ, SKP (3D models)
- **Coverage**: KALLAX, HEMNES, BESTÅ, BEKANT, and many more series
- **Dual units**: Both inches and centimeters
- **Includes**: Weight capacities
- **Note**: Good for manual reference but not easily machine-parseable without scraping
**Source**: https://www.dimensions.com/collection/ikea-furniture
---
## 3. IKEA 3D Models
### Web-Sourced GLB Models (Best Path for Three.js)
IKEA product pages with a "View in 3D" button serve simplified, Draco-compressed GLB (binary glTF) files. These are:
- **Format**: GLB (binary glTF 2.0) — native Three.js format
- **Quality**: "Lowest quality versions" — simplified from manufacturing models for web delivery
- **Properties**: Scaled in metric, basic materials applied, KHR_Texture_Transform extension
- **Compatibility**: Direct import into Three.js via `GLTFLoader` + `DRACOLoader`
#### Tools to Download IKEA GLB Models
| Tool | Type | Stars | Status | License |
|------|------|-------|--------|---------|
| [IKEA 3D Model Download Button](https://github.com/apinanaivot/IKEA-3D-Model-Download-Button) | Tampermonkey userscript | 941 | Active (Jan 2026) | — |
| [IKEA 3D Model Batch Downloader](https://github.com/apinanaivot/IKEA-3d-model-batch-downloader) | Python (Selenium+BS4) | — | Active | GPL-3.0 |
| [IKEA Browser for Blender](https://extensions.blender.org/add-ons/ikea-browser/) | Blender addon | 95k downloads | Active (v0.4.0, Oct 2025) | GPL-3.0 |
| [Chrome Extension](https://chromewebstore.google.com/detail/ikea-3d-models-downloader/kihdhjecfjagoplfnnpdbbnhkjhnlfeg) | Chrome extension | — | Active | — |
The **batch downloader** workflow:
1. Give it an IKEA category URL (e.g., `https://www.ikea.com/fi/fi/cat/chairs-fu002/`)
2. Scrapes product links → extracts 3D model URLs → downloads GLB files
3. Stores metadata in SQLite database
4. Deduplication built-in
**Note**: Not all IKEA products have 3D models. Only items with the "View in 3D" button are available.
### Official IKEA 3D Assembly Dataset (GitHub)
- **Source**: https://github.com/IKEA/IKEA3DAssemblyDataset
- **Items**: Only 5 products (LACK, EKET, BEKVÄM, DALFRED)
- **Formats**: GLB/glTF + OBJ + PDF assembly instructions
- **License**: CC BY-NC-SA 4.0 — **non-commercial only, research purposes**
- **Verdict**: Too few items and restrictive license. Not suitable for our use.
### Sweet Home 3D IKEA Libraries
- **180 models**: https://3deshop.blogscopia.com/180-ikea-models-for-sweethome3d/
- **342 models (bundle)**: https://3deshop.blogscopia.com/ikea-bundle-342-models/
- **Format**: SH3F (Sweet Home 3D format) — would need conversion
- **Quality**: Community-created, based on real IKEA products
- **Note**: Format is proprietary to Sweet Home 3D, would require extraction/conversion
### Third-Party Model Sources
- **Sketchfab**: Search "IKEA" — many free community models, various licenses
- **TurboSquid**: Free IKEA models available
- **Clara.io**: Some IKEA models
- **Trimble 3D Warehouse**: IKEA models available (SketchUp format)
---
## 4. Web Scraping Feasibility
### Existing Scrapers
| Project | Stack | Notes |
|---------|-------|-------|
| [IKEA Scraper](https://github.com/Abdelrahman-Hekal/IKEA_Scraper) | Python | Full scraper for ikea.com |
| [ikea-webscraper](https://github.com/gamladz/ikea-webscraper) | Selenium | Product info extraction |
| [IKEA-project](https://github.com/furkansenn/IKEA-project) | Selenium | Product scraping |
| [ikea-scraper](https://github.com/bonzi/ikea-scraper) | — | IKEA data extraction |
### Technical Challenges
- IKEA uses heavy client-side rendering (React/Next.js) requiring browser automation (Selenium)
- Anti-bot protections: CAPTCHAs, rate limiting, IP blocking
- Dimension data is embedded in product pages in varying formats
- Product IDs are 8-digit numbers but URL structure varies by locale
### Legal Considerations
- IKEA's Terms of Service prohibit automated scraping
- IKEA actively protects trademarks and trade dress under US Trademark Act
- For personal/non-commercial home planning use, tools like the Blender extension operate in a gray area
- The HuggingFace CommerceTXT dataset (CC0) is the safest legal path for product data
---
## 5. Standard IKEA Dimensions Reference
Manually verified dimensions for the most common IKEA product lines:
### Storage
| Product | Width (cm) | Depth (cm) | Height (cm) |
|---------|-----------|-----------|------------|
| KALLAX 1×4 | 42 | 39 | 147 |
| KALLAX 2×2 | 77 | 39 | 77 |
| KALLAX 2×4 | 77 | 39 | 147 |
| KALLAX 4×4 | 147 | 39 | 147 |
| BILLY bookcase (standard) | 80 | 28 | 202 |
| BILLY bookcase (narrow) | 40 | 28 | 202 |
| BILLY bookcase (short) | 80 | 28 | 106 |
| HEMNES 6-drawer dresser | 108 | 50 | 131 |
| HEMNES 3-drawer dresser | 108 | 50 | 96 |
| HEMNES bookcase | 90 | 37 | 197 |
| BESTÅ TV bench | 120/180 | 42 | 38 |
| MALM 6-drawer dresser | 80 | 48 | 123 |
| MALM 4-drawer dresser | 80 | 48 | 100 |
| PAX wardrobe (standard) | 100/150/200 | 58 | 201/236 |
### Tables
| Product | Width (cm) | Depth (cm) | Height (cm) |
|---------|-----------|-----------|------------|
| LACK side table | 55 | 55 | 45 |
| LACK coffee table | 90 | 55 | 45 |
| LACK TV bench | 90 | 26 | 45 |
| LISABO desk | 118 | 45 | 74 |
| BEKANT desk (rect) | 120/140/160 | 80 | 65-85 |
| LINNMON/ALEX desk | 150 | 75 | 73 |
| MELLTORP dining table | 125 | 75 | 74 |
| EKEDALEN ext. table | 120-180 | 80 | 75 |
### Seating
| Product | Width (cm) | Depth (cm) | Height (cm) |
|---------|-----------|-----------|------------|
| POÄNG armchair | 68 | 82 | 100 |
| STRANDMON wing chair | 82 | 96 | 101 |
| KLIPPAN 2-seat sofa | 180 | 88 | 66 |
| EKTORP 3-seat sofa | 218 | 88 | 88 |
| KIVIK 3-seat sofa | 228 | 95 | 83 |
| MARKUS office chair | 62 | 60 | 129-140 |
### Beds
| Product | Width (cm) | Depth (cm) | Height (cm) |
|---------|-----------|-----------|------------|
| MALM bed (queen, 160) | 160 | 209 | 92 (headboard) |
| MALM bed (king, 180) | 180 | 209 | 92 (headboard) |
| MALM bed (single, 90) | 90 | 209 | 92 (headboard) |
| HEMNES bed (queen) | 163 | 211 | 66 (headboard 112) |
| KURA reversible bed | 99 | 209 | 116 |
| SUNDVIK child bed | 80 | 167 | 83 |
### Kitchen
| Product | Standard Width (cm) | Depth (cm) | Height (cm) |
|---------|-----------|-----------|------------|
| METOD base cabinet | 60/80 | 60 | 80 |
| METOD wall cabinet | 60/80 | 37 | 60/80/100 |
| METOD tall cabinet | 60 | 60 | 200/220 |
| KNOXHULT base cabinet | 120/180 | 61 | 85 |
| VADHOLMA kitchen island | 126 | 79 | 90 |
---
## 6. Recommendations for Our Project
### Recommended Approach: Tiered Strategy
#### Tier 1: Curated IKEA Catalog (Immediate)
Extend `data/furniture-catalog.json` with IKEA-specific items:
- Add 30-50 most popular IKEA products with verified dimensions
- Use our existing procedural mesh format (box geometry parts)
- Add `ikeaId` field (8-digit product number) for future linking
- Add `ikeaSeries` field (KALLAX, BILLY, MALM, etc.)
- Add `ikeaUrl` field for reference
**Schema extension**:
```json
{
"id": "ikea-kallax-2x4",
"name": "KALLAX Regal 2×4",
"ikeaId": "80275887",
"ikeaSeries": "KALLAX",
"ikeaUrl": "https://www.ikea.com/de/de/p/kallax-regal-weiss-80275887/",
"category": "storage",
"rooms": ["wohnzimmer", "arbeitszimmer", "kinderzimmer"],
"dimensions": { "width": 0.77, "depth": 0.39, "height": 1.47 },
"variants": [
{ "color": "white", "hex": "#ffffff" },
{ "color": "black-brown", "hex": "#3c3028" },
{ "color": "white-stained-oak", "hex": "#d4c4a8" }
],
"mesh": { ... }
}
```
**Effort**: ~2-3 hours to add 30 items manually from IKEA website
**Risk**: None — uses verified public dimensions
#### Tier 2: GLB Model Import (Enhancement)
Add optional GLB model loading to the renderer:
- Use Three.js `GLTFLoader` + `DRACOLoader` to load IKEA GLB files
- Users download GLB files themselves (personal use) via browser extension
- Store in local `models/` directory
- Fall back to procedural mesh if GLB not available
```javascript
// In catalog entry:
{
"id": "ikea-kallax-2x4",
"model3d": "models/ikea/kallax-2x4.glb", // optional
"mesh": { ... } // fallback procedural geometry
}
```
**Effort**: ~4-6 hours for GLTFLoader integration
**Risk**: Low — GLB loading is standard Three.js functionality
#### Tier 3: Dataset Import Tool (Future)
Build a converter that parses the HuggingFace CommerceTXT dataset:
- Extract product dimensions from `@SPECS` section
- Map categories to our catalog format
- Auto-generate procedural meshes from dimensions
- Run offline as a data pipeline
**Effort**: ~1-2 days
**Risk**: Medium — dimension parsing from free-text specs varies in reliability
### What NOT to Do
1. **Don't scrape IKEA live** — Legal risk, fragile, unnecessary when datasets exist
2. **Don't depend on undocumented APIs** — They change without notice
3. **Don't bundle IKEA 3D models** — Trademark/IP issues. Let users provide their own
4. **Don't use the IKEA 3D Assembly Dataset for the app** — License is NC-SA research only
---
## 7. Key Sources
| Resource | URL | Best For |
|----------|-----|----------|
| HuggingFace IKEA US | https://huggingface.co/datasets/tsazan/ikea-us-commercetxt | Dimension data (CC0) |
| Kaggle IKEA SA | https://www.kaggle.com/datasets/ahmedkallam/ikea-sa-furniture-web-scraping | Structured CSV with dims |
| Dimensions.com IKEA | https://www.dimensions.com/collection/ikea-furniture | Reference dimensions + drawings |
| 3D Batch Downloader | https://github.com/apinanaivot/IKEA-3d-model-batch-downloader | Downloading GLB models |
| Blender IKEA Browser | https://extensions.blender.org/add-ons/ikea-browser/ | Model inspection/conversion |
| IKEA API Blog | https://sbmueller.github.io/posts/ikea/ | Understanding IKEA endpoints |
| Sweet Home 3D IKEA | https://3deshop.blogscopia.com/ikea-bundle-342-models/ | Pre-made 3D models (needs conversion) |

View File

@@ -45,6 +45,19 @@ export class CatalogPanel {
this._itemList.className = 'catalog-items'; this._itemList.className = 'catalog-items';
this.container.appendChild(this._itemList); this.container.appendChild(this._itemList);
// Create custom button
this._createBtn = document.createElement('button');
this._createBtn.className = 'catalog-create-btn';
this._createBtn.textContent = '+ Create Custom Furniture';
this._createBtn.addEventListener('click', () => this._showCreateForm());
this.container.appendChild(this._createBtn);
// Create form container (hidden initially)
this._createFormContainer = document.createElement('div');
this._createFormContainer.className = 'catalog-create-form';
this._createFormContainer.style.display = 'none';
this.container.appendChild(this._createFormContainer);
this._renderCategories(); this._renderCategories();
this._renderItems(); this._renderItems();
} }
@@ -207,6 +220,164 @@ export class CatalogPanel {
return rooms[0].id; return rooms[0].id;
} }
// ---- Custom furniture creator ----
_showCreateForm() {
this._createBtn.style.display = 'none';
this._createFormContainer.style.display = '';
this._buildCreateForm();
}
_hideCreateForm() {
this._createBtn.style.display = '';
this._createFormContainer.style.display = 'none';
this._createFormContainer.innerHTML = '';
}
_buildCreateForm() {
const catalog = this.renderer.catalogData;
const categories = catalog?.categories || [];
const f = this._createFormContainer;
f.innerHTML = '';
// Name
const nameLabel = document.createElement('label');
nameLabel.textContent = 'Name';
f.appendChild(nameLabel);
const nameInput = document.createElement('input');
nameInput.type = 'text';
nameInput.placeholder = 'e.g. Custom Shelf';
f.appendChild(nameInput);
// Dimensions
const dimLabel = document.createElement('label');
dimLabel.textContent = 'Dimensions (meters)';
f.appendChild(dimLabel);
const dimRow = document.createElement('div');
dimRow.className = 'catalog-create-dims';
const makeDimField = (label, value) => {
const wrap = document.createElement('div');
const lbl = document.createElement('label');
lbl.textContent = label;
wrap.appendChild(lbl);
const inp = document.createElement('input');
inp.type = 'number';
inp.step = '0.05';
inp.min = '0.05';
inp.max = '10';
inp.value = value;
wrap.appendChild(inp);
dimRow.appendChild(wrap);
return inp;
};
const widthInput = makeDimField('W', '0.8');
const depthInput = makeDimField('D', '0.4');
const heightInput = makeDimField('H', '0.8');
f.appendChild(dimRow);
// Color
const colorLabel = document.createElement('label');
colorLabel.textContent = 'Color';
f.appendChild(colorLabel);
const colorRow = document.createElement('div');
colorRow.className = 'catalog-create-color-row';
const colorPicker = document.createElement('input');
colorPicker.type = 'color';
colorPicker.value = '#8899aa';
const colorText = document.createElement('input');
colorText.type = 'text';
colorText.value = '#8899aa';
colorPicker.addEventListener('input', () => { colorText.value = colorPicker.value; });
colorText.addEventListener('input', () => {
if (/^#[0-9a-f]{6}$/i.test(colorText.value)) colorPicker.value = colorText.value;
});
colorRow.appendChild(colorPicker);
colorRow.appendChild(colorText);
f.appendChild(colorRow);
// Category
const catLabel = document.createElement('label');
catLabel.textContent = 'Category';
f.appendChild(catLabel);
const catSelect = document.createElement('select');
for (const cat of categories) {
const opt = document.createElement('option');
opt.value = cat;
opt.textContent = cat.charAt(0).toUpperCase() + cat.slice(1);
catSelect.appendChild(opt);
}
f.appendChild(catSelect);
// Actions
const actions = document.createElement('div');
actions.className = 'catalog-create-actions';
const cancelBtn = document.createElement('button');
cancelBtn.className = 'btn-cancel';
cancelBtn.textContent = 'Cancel';
cancelBtn.addEventListener('click', () => this._hideCreateForm());
const submitBtn = document.createElement('button');
submitBtn.className = 'btn-submit';
submitBtn.textContent = 'Add to Catalog';
submitBtn.addEventListener('click', () => {
const name = nameInput.value.trim();
if (!name) { nameInput.focus(); return; }
const w = parseFloat(widthInput.value) || 0.8;
const d = parseFloat(depthInput.value) || 0.4;
const h = parseFloat(heightInput.value) || 0.8;
const color = colorText.value || '#8899aa';
const category = catSelect.value;
this._addCustomItem({ name, width: w, depth: d, height: h, color, category });
this._hideCreateForm();
});
actions.appendChild(cancelBtn);
actions.appendChild(submitBtn);
f.appendChild(actions);
nameInput.focus();
}
_addCustomItem({ name, width, depth, height, color, category }) {
const catalog = this.renderer.catalogData;
if (!catalog) return;
// Generate unique id
const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '');
let id = `custom-${slug}`;
let n = 1;
while (this.renderer._catalogIndex.has(id)) {
id = `custom-${slug}-${++n}`;
}
// Build a simple box mesh matching the catalog format
const item = {
id,
name,
category,
rooms: [],
dimensions: { width, depth, height },
mesh: {
type: 'group',
parts: [
{
name: 'body',
geometry: 'box',
size: [width, height, depth],
position: [0, height / 2, 0],
color
}
]
}
};
catalog.items.push(item);
this.renderer._catalogIndex.set(id, item);
// Refresh display
this._renderItems();
}
// ---- Events ---- // ---- Events ----
_bindEvents() { _bindEvents() {

View File

@@ -207,6 +207,103 @@
font-size: 12px; font-size: 12px;
} }
/* Custom furniture creator */
.catalog-create-btn {
display: block;
width: calc(100% - 24px);
margin: 8px 12px;
padding: 8px;
border: 1px dashed #4a90d9;
border-radius: 4px;
background: transparent;
color: #4a90d9;
cursor: pointer;
font-size: 12px;
font-weight: 500;
}
.catalog-create-btn:hover {
background: #e8f0fe;
}
.catalog-create-form {
padding: 0 12px 12px;
border-top: 1px solid #eee;
margin-top: 4px;
}
.catalog-create-form label {
display: block;
font-size: 11px;
color: #666;
margin-top: 8px;
margin-bottom: 2px;
}
.catalog-create-form input,
.catalog-create-form select {
width: 100%;
padding: 5px 8px;
border: 1px solid #ccc;
border-radius: 3px;
font-size: 12px;
outline: none;
}
.catalog-create-form input:focus,
.catalog-create-form select:focus {
border-color: #4a90d9;
}
.catalog-create-dims {
display: flex;
gap: 6px;
}
.catalog-create-dims > div {
flex: 1;
}
.catalog-create-dims label {
margin-top: 0 !important;
}
.catalog-create-color-row {
display: flex;
align-items: center;
gap: 8px;
}
.catalog-create-color-row input[type="color"] {
width: 32px;
height: 28px;
padding: 1px;
border: 1px solid #ccc;
border-radius: 3px;
cursor: pointer;
flex-shrink: 0;
}
.catalog-create-color-row input[type="text"] {
flex: 1;
}
.catalog-create-actions {
display: flex;
gap: 6px;
margin-top: 10px;
}
.catalog-create-actions button {
flex: 1;
padding: 6px;
border: 1px solid #ccc;
border-radius: 3px;
font-size: 12px;
cursor: pointer;
}
.catalog-create-actions .btn-submit {
background: #4a90d9;
color: #fff;
border-color: #4a90d9;
}
.catalog-create-actions .btn-submit:hover {
background: #3a7bc8;
}
.catalog-create-actions .btn-cancel {
background: #fff;
}
.catalog-create-actions .btn-cancel:hover {
background: #f0f0f0;
}
.export-section { .export-section {
margin-top: 16px; margin-top: 16px;
padding-top: 12px; padding-top: 12px;