Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ jobs:

- run: pnpm install --frozen-lockfile

- run: pnpm format:check

- run: pnpm build

- run: pnpm lint
Expand Down
7 changes: 7 additions & 0 deletions .oxfmtrc.jsonc
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"$schema": "./node_modules/oxfmt/configuration_schema.json",
"printWidth": 100,
"singleQuote": true,
"semi": false,
"trailingComma": "all",
}
7 changes: 7 additions & 0 deletions .oxlintrc.jsonc
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"ignorePatterns": [
"**/coverage/**",
"**/dist/**",
],
}
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- `@devicekit/devices`: bundled profiles now include `releaseYear`, and the preset exports use `PresetDeviceProfile` so selectors can sort models chronologically without handling a missing year.

## [0.2.3] - 2026-09-08

### Fixed
Expand Down
28 changes: 14 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,20 +66,20 @@ The package READMEs contain the full API references.

171 profiles: 63 iOS, 86 Android, 22 HarmonyOS. Folding phones get one entry per screen.

| Manufacturer | Devices |
| --- | ---: |
| Apple | 63 |
| Samsung | 42 |
| Google | 34 |
| Huawei | 22 |
| Motorola | 3 |
| Microsoft | 2 |
| OnePlus | 2 |
| LG | 1 |
| Nothing | 1 |
| Xiaomi | 1 |

Manufacturer counts are derived from device names because profiles do not have a manufacturer field. `CLASSIC_DEVICES` contains 19 commonly used profiles for shorter selectors.
| Manufacturer | Profiles | Representative models |
| --- | ---: | --- |
| Apple | 63 | iPhone 17 Pro, iPhone 16 Pro, iPhone X, iPad Pro M4 |
| Samsung | 42 | Galaxy S24 Ultra, Galaxy Z Fold 7, Galaxy Z Flip 7 |
| Google | 34 | Pixel 10 Pro, Pixel 9 Pro Fold, Pixel Tablet |
| Huawei | 22 | HUAWEI Mate 80, HUAWEI Pura 80 Pro, HUAWEI Mate X6 (inner), HUAWEI Mate X6 (outer) |
| Motorola | 3 | Motorola Razr+, Moto G4, Moto G Power |
| Microsoft | 2 | Surface Duo |
| OnePlus | 2 | OnePlus 12, OnePlus Open |
| LG | 1 | LG Optimus L70 |
| Nothing | 1 | Nothing Phone 2 |
| Xiaomi | 1 | Xiaomi 14 |

Every preset includes a `releaseYear`, so a picker can sort models chronologically. Manufacturer counts are derived from device names because profiles do not have a manufacturer field. `CLASSIC_DEVICES` contains 19 commonly used profiles for shorter selectors.

## Browser and framework support

Expand Down
28 changes: 14 additions & 14 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,20 +66,20 @@ defineDeviceFrame()

共 171 个机型:iOS 63、Android 86、HarmonyOS 22。折叠机内外屏各算一条。

| 厂商 | 数量 |
| --- | ---: |
| Apple | 63 |
| Samsung | 42 |
| Google | 34 |
| Huawei | 22 |
| Motorola | 3 |
| Microsoft | 2 |
| OnePlus | 2 |
| LG | 1 |
| Nothing | 1 |
| Xiaomi | 1 |

机型数据没有厂商字段,因此厂商数量按机型名称统计。`CLASSIC_DEVICES` 另提供 19 个常用机型,适合空间较小的选择器。
| 厂商 | 数量 | 代表机型 |
| --- | ---: | --- |
| Apple | 63 | iPhone 17 Pro、iPhone 16 Pro、iPhone X、iPad Pro M4 |
| Samsung | 42 | Galaxy S24 Ultra、Galaxy Z Fold 7、Galaxy Z Flip 7 |
| Google | 34 | Pixel 10 Pro、Pixel 9 Pro Fold、Pixel Tablet |
| Huawei | 22 | HUAWEI Mate 80、HUAWEI Pura 80 Pro、HUAWEI Mate X6 (inner)、HUAWEI Mate X6 (outer) |
| Motorola | 3 | Motorola Razr+、Moto G4、Moto G Power |
| Microsoft | 2 | Surface Duo |
| OnePlus | 2 | OnePlus 12、OnePlus Open |
| LG | 1 | LG Optimus L70 |
| Nothing | 1 | Nothing Phone 2 |
| Xiaomi | 1 | Xiaomi 14 |

每个预设都有 `releaseYear`,机型选择器可以按年份排序。机型数据没有厂商字段,因此厂商数量按机型名称统计。`CLASSIC_DEVICES` 另提供 19 个常用机型,适合空间较小的选择器。

## 浏览器和框架支持

Expand Down
7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,20 @@
"scripts": {
"build": "pnpm -r build",
"check-types": "pnpm -r check-types",
"format": "oxfmt packages/devices/src/presets/ios.ts packages/devices/src/presets/android.ts packages/devices/src/presets/harmony.ts",
"format:check": "oxfmt --check packages/devices/src/presets/ios.ts packages/devices/src/presets/android.ts packages/devices/src/presets/harmony.ts",
"test": "pnpm -r test && pnpm run test:scripts",
"test:scripts": "node --test scripts/*.test.mjs",
"test:coverage": "pnpm -r test:coverage",
"lint": "pnpm -r lint",
"lint": "pnpm -r lint && pnpm lint:oxc",
"lint:oxc": "oxlint .",
"verify:pack": "pnpm -r build && node scripts/verify-pack.mjs",
"images": "node scripts/render-readme-images.mjs"
},
"devDependencies": {
"eslint": "^10.2.1",
"oxfmt": "0.67.0",
"oxlint": "1.82.0",
"typescript": "5.9.2",
"typescript-eslint": "^8.58.2"
},
Expand Down
16 changes: 9 additions & 7 deletions packages/devices/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ const profile = {
os: 'ios',
screen: { width: 402, height: 874 },
pixelRatio: 3,
releaseYear: 2024,
system: 'iOS 18.5',
statusBarHeight: 54,
safeAreaInsets: { top: 62, bottom: 34 },
Expand All @@ -70,7 +71,7 @@ const profile = {
}
```

Only `name`, `os`, `screen`, and `pixelRatio` are required. `screen` uses CSS pixels and is stored in portrait orientation. `orientedScreen()` swaps its dimensions for landscape.
Only `name`, `os`, `screen`, and `pixelRatio` are required. Presets also include `releaseYear`, which is the model's release year and can be used to sort a picker. `screen` uses CSS pixels and is stored in portrait orientation. `orientedScreen()` swaps its dimensions for landscape.

Orientation-specific status bars, navigation bars, and safe-area insets are stored separately. The status bar height and the top safe-area inset are separate fields because they can differ on devices with a cutout. `cutout` describes the rendered shape; safe-area fields describe the space available to content.

Expand Down Expand Up @@ -120,12 +121,12 @@ const version = systemVersion(profile)

| Export | Type | Description |
| --- | --- | --- |
| `DEVICES` | `readonly DeviceProfile[]` | Complete table in iOS, Android, HarmonyOS order |
| `IOS_DEVICES` | `readonly DeviceProfile[]` | iOS profiles |
| `ANDROID_DEVICES` | `readonly DeviceProfile[]` | Android profiles |
| `HARMONY_DEVICES` | `readonly DeviceProfile[]` | HarmonyOS profiles |
| `CLASSIC_DEVICES` | `readonly DeviceProfile[]` | 19 profiles for short selectors |
| `DEFAULT_DEVICE` | `DeviceProfile` | iPhone X profile |
| `DEVICES` | `readonly PresetDeviceProfile[]` | Complete table in iOS, Android, HarmonyOS order |
| `IOS_DEVICES` | `readonly PresetDeviceProfile[]` | iOS profiles |
| `ANDROID_DEVICES` | `readonly PresetDeviceProfile[]` | Android profiles |
| `HARMONY_DEVICES` | `readonly PresetDeviceProfile[]` | HarmonyOS profiles |
| `CLASSIC_DEVICES` | `readonly PresetDeviceProfile[]` | 19 profiles for short selectors |
| `DEFAULT_DEVICE` | `PresetDeviceProfile` | iPhone X profile |
| `PLATFORM_DEFAULTS` | `Record<DeviceOS, ...>` | Default bar and shell values by platform |
| `DEVICE_NAMES` | `const object` | Typed name constants generated from `DEVICES` |

Expand Down Expand Up @@ -154,6 +155,7 @@ const version = systemVersion(profile)
| Type | Description |
| --- | --- |
| `DeviceProfile` | A stored profile with optional device-specific fields |
| `PresetDeviceProfile` | A bundled profile with a required `releaseYear` |
| `ResolvedDevice` | A profile with defaults applied |
| `DeviceOS` | `'ios' \| 'android' \| 'harmony'` |
| `DeviceFormFactor` | `'phone' \| 'tablet'` |
Expand Down
13 changes: 7 additions & 6 deletions packages/devices/README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ const device = findDevice(DEVICE_NAMES.iPhone_16_Pro) ?? DEFAULT_DEVICE
os: 'ios',
screen: { width: 402, height: 874 }, // 物理屏,竖屏方向
pixelRatio: 3,
releaseYear: 2024, // 机型发布年份
system: 'iOS 18.5',
statusBarHeight: 54, // 画出来的那条状态栏
safeAreaInsets: { top: 62, bottom: 34 }, // 竖屏,实测值
Expand Down Expand Up @@ -149,12 +150,12 @@ import { deviceUserAgent, systemVersion } from '@devicekit/devices'

| 导出 | 类型 | 是什么 |
| --- | --- | --- |
| `DEVICES` | `readonly DeviceProfile[]` | 整张表,iOS、Android、HarmonyOS 顺序 |
| `IOS_DEVICES` | `readonly DeviceProfile[]` | iOS 那部分 |
| `ANDROID_DEVICES` | `readonly DeviceProfile[]` | Android 那部分 |
| `HARMONY_DEVICES` | `readonly DeviceProfile[]` | HarmonyOS 那部分 |
| `CLASSIC_DEVICES` | `readonly DeviceProfile[]` | 手选的不到 20 台,同一批对象,给短列表用 |
| `DEFAULT_DEVICE` | `DeviceProfile` | 没指定机型时画的那台(iPhone X) |
| `DEVICES` | `readonly PresetDeviceProfile[]` | 整张表,iOS、Android、HarmonyOS 顺序 |
| `IOS_DEVICES` | `readonly PresetDeviceProfile[]` | iOS 那部分 |
| `ANDROID_DEVICES` | `readonly PresetDeviceProfile[]` | Android 那部分 |
| `HARMONY_DEVICES` | `readonly PresetDeviceProfile[]` | HarmonyOS 那部分 |
| `CLASSIC_DEVICES` | `readonly PresetDeviceProfile[]` | 手选的不到 20 台,同一批对象,给短列表用 |
| `DEFAULT_DEVICE` | `PresetDeviceProfile` | 没指定机型时画的那台(iPhone X) |
| `PLATFORM_DEFAULTS` | `Record<DeviceOS, {...}>` | 各平台的状态栏、导航栏和机身默认值 |
| `DEVICE_NAMES` | `{ [key: string]: string }`(as const) | 每个 `DEVICES[number].name`,键是 `deviceNameKey(name)`——生成文件,见下 |

Expand Down
9 changes: 9 additions & 0 deletions packages/devices/src/devices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,8 @@ export interface DeviceProfile {
formFactor?: DeviceFormFactor
/** Shown in the device picker, e.g. "iOS 18.0". Also feeds the generated user agent. */
system?: string
/** Calendar year in which this model was released. */
releaseYear?: number
/**
* What a page emulating this device should report as `navigator.userAgent`.
* Omitted = generated from `os` and `system` — see deviceUserAgent().
Expand Down Expand Up @@ -148,6 +150,11 @@ export interface DeviceProfile {
shell?: Partial<DeviceShell>
}

/** A bundled profile, whose release year is known and always present. */
export interface PresetDeviceProfile extends DeviceProfile {
releaseYear: number
}

/**
* Fallbacks for a profile that leaves a field out, and for hosts that only know
* the platform.
Expand Down Expand Up @@ -200,6 +207,7 @@ export interface ResolvedDevice {
pixelRatio: number
formFactor: DeviceFormFactor
system: string
releaseYear?: number
userAgent: string
statusBarHeight: number
statusBarHeightLandscape: number
Expand Down Expand Up @@ -254,6 +262,7 @@ export function resolveDevice(profile: DeviceProfile): ResolvedDevice {
pixelRatio: profile.pixelRatio,
formFactor: profile.formFactor ?? 'phone',
system: profile.system ?? '',
...(profile.releaseYear === undefined ? {} : { releaseYear: profile.releaseYear }),
userAgent: profile.userAgent ?? deviceUserAgent(profile),
statusBarHeight,
statusBarHeightLandscape,
Expand Down
1 change: 1 addition & 0 deletions packages/devices/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export {
type DeviceFormFactor,
type DeviceOS,
type DeviceProfile,
type PresetDeviceProfile,
type DeviceShell,
type EdgeInsets,
type HomeButtonSpec,
Expand Down
5 changes: 5 additions & 0 deletions packages/devices/src/presets.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ describe('the table is well formed', () => {
expect(broken.map((d) => d.name)).toEqual([])
})

it('records the release year for every device so consumers can sort the table chronologically', () => {
const missingOrInvalid = DEVICES.filter((d) => !Number.isInteger(d.releaseYear) || d.releaseYear! < 2000 || d.releaseYear! > 2100)
expect(missingOrInvalid.map((d) => d.name)).toEqual([])
})

// Rotating is what the orientation argument is for. A source table that also
// ships a pre-rotated copy of a device leaves two rows here where one turns
// into the other, and the pre-rotated one then rotates backwards.
Expand Down
Loading