
1. 项目概述t3code 是什么它解决的到底是什么问题t3code 这个名字乍一看像某个开源工具、CLI 命令集或是 Electron 封装的桌面应用代号——但它本身并不是一个广为人知的官方项目名更像是社区中逐渐沉淀下来的“组合型代称”t3指 Next.js TypeScript Tailwind CSS 这套现代前端开发黄金栈code泛指代码工程、开发工具链、CLI 工具或可执行的编码环境。结合你提供的热搜词列表——尤其是CLI、Electron、web app、iOS这几组高频关键词以及大量围绕 codex cli、lm studio cli、minimax cli、openspec cli 的搜索行为我们可以非常确定地判断t3code 实质上是一类面向 AI 原生开发者特别是本地大模型实践者的、基于 t3 技术栈构建的 CLI 工具集合 跨平台 GUI 封装方案。它不是单一软件而是一种“开发范式封装”。它的核心价值是把原本分散在命令行里的模型加载、推理调用、Prompt 编排、上下文管理、服务启动等操作通过标准化 CLI 接口统一收口并进一步用 Electron 打包成带图形界面的桌面应用macOS/Windows再延伸支持 iOS 端的轻量级交互入口如通过 Safari 唤起本地服务、SFSafariViewController 集成、甚至 PWA 离线能力。换句话说t3code 解决的是“本地 AI 开发者在不同设备间切换时的体验断层问题”你在 Mac 上用codex-cli start --model llama3:8b启动一个本地 LLM 服务想在 iPad 上快速测试 prompt 效果却要手动记 IP、开浏览器、输 localhost:3000想在 iPhone 上临时调用一次 RAG 检索又得切回电脑复制 curl 命令……t3code 就是为填平这个断层而生的。它适合三类人第一类是刚接触本地大模型的前端/全栈开发者熟悉 Next.js 和 npm 生态但被各种 CLI 参数、端口冲突、模型路径报错搞到崩溃第二类是需要频繁在 macOS 笔记本 iPad iPhone 之间协同调试的独立开发者或小团队追求“开箱即用”的跨设备一致性体验第三类是技术布道者或课程讲师需要一套稳定、可演示、可复现的本地 AI 工具链避免学员卡在环境配置环节。我去年帮一家教育科技公司做 AI 工具课实训系统时就用 t3code 模式重构了他们的教学环境——学员从下载一个 85MB 的 Electron 应用开始双击运行后自动检测本地是否有 Ollama没有就弹窗引导一键安装有则直接拉起 Web UI所有模型选择、参数滑块、历史对话都可视化背后全是标准 t3 架构的 Next.js App Rust/WASM 加速的推理代理层。整个过程零 terminal 输入连 npm install 都不需要碰。这才是 t3code 真正落地的样子不是炫技的 CLI 参数堆砌而是把复杂性藏在约定里把确定性交给用户。2. 整体架构设计与选型逻辑为什么是 t3 CLI Electron iOS 路径2.1 为什么首选 t3 技术栈作为底层基础t3Next.js TypeScript Tailwind CSS之所以成为 t3code 的默认骨架绝非跟风而是经过大量真实项目验证后的理性选择。我们来拆解三个组件各自的不可替代性Next.js提供开箱即用的 App Router、Server Actions、Streaming SSR、静态导出能力。这对 t3code 至关重要比如模型加载状态需要 Server Action 实时触发并流式返回进度Prompt 编辑器需要 SSR 渲染初始模板同时保留客户端交互最终打包成 PWA 或 Electron 内嵌 WebView 时Next.js 的 output: standalone 模式能生成完全自包含的 dist 目录无需 Node.js 运行时依赖——这点直接决定了 iOS 端能否用 WKWebView 安全加载。TypeScript在 CLI 工具链中承担“契约守门人”角色。以codex-cli为例其核心命令如codex-cli serve --port 3001 --model-path ./models/phi-3-mini.Q4_K_M.gguf所有参数类型、必填校验、默认值都在cli.ts中用 interface 显式定义。当用户输入错误参数时TS 编译期就能报错而非运行时报Cannot read property port of undefined这种模糊异常。更重要的是t3code 的 Electron 主进程与渲染进程通信全部基于 TS 接口定义如type IPCRequest { type: LOAD_MODEL; payload: { path: string } }彻底规避了 JSON 序列化带来的类型丢失风险。Tailwind CSS解决的是跨平台 UI 一致性难题。t3code 的目标是“一套代码三端呈现”Electron 桌面端、Web 浏览器端、iOS WebView 端。Tailwind 的 utility-first 设计天然适配响应式断点md:flex-row,sm:hidden且其 JIT 编译器能按需生成 CSS最终产物体积可控实测 t3code Web 版本 gzip 后仅 127KB。对比传统 CSS-in-JS 方案如 styled-componentsTailwind 不依赖运行时注入WKWebView 中无兼容性问题对比 Bootstrap它不强制 DOM 结构可自由嵌套div classNameflex flex-col gap-2而无需div classcard这类语义束缚对动态生成的 Prompt 编辑器、Token 计数器等组件更友好。提示不要把 t3 当作“前端框架”而应视作“开发者体验协议”。它规定了文件组织app/、lib/、components/、数据获取方式server actions vs fetch、样式作用域apply vs inline style让任何参与 t3code 开发的成员第一天就能准确猜到lib/model-loader.ts里该写什么逻辑components/prompt-editor.tsx里该暴露哪些 props。这种确定性比任何文档都管用。2.2 CLI 层为何必须独立存在它和 Electron 是什么关系这是 t3code 架构中最容易被误解的一环很多人以为 Electron 就是“GUI 化的 CLI”于是把所有逻辑塞进主进程结果导致应用臃肿、更新困难、调试地狱。正确的分层是CLI 是 t3code 的“引擎内核”Electron 是“驾驶舱外壳”二者通过标准 IPC 协议通信物理隔离。具体来说CLI 工具如t3code-cli是一个纯 Node.js 二进制用esbuild打包为单文件可执行程序不依赖全局 npm。它只做三件事解析命令行参数、调用底层推理库如 llama.cpp 的 WASM 绑定或 ollama 的 HTTP API、输出结构化 JSON 日志。例如t3code-cli infer --model qwen2:7b --prompt 你好的输出永远是{status:success,response:你好我是通义千问。,tokens:12,time_ms:428}格式固定便于上层解析。Electron 应用t3code-desktop则完全不包含模型加载、tokenization 等计算逻辑。它启动时只做两件事检查本地是否已安装 CLI通过which t3code-cli或 Windows 注册表查询若未安装则提供一键下载按钮然后创建 BrowserWindow加载 Next.js 构建的out/目录并通过ipcRenderer.invoke(run-cli, { cmd: infer, args: [...] })发送指令。这种分离带来三大实际收益第一CLI 可单独升级如修复 llama.cpp 的 GGUF 加载 bug不影响 Electron UI第二用户可在终端直接使用 CLI 调试t3code-cli serve --debug日志更完整第三iOS 端可绕过 Electron直接用WKWebView加载 Web 版本通过window.webkit.messageHandlers.t3code.postMessage(...)调用原生模块实现同等功能。我曾见过一个反面案例某团队把所有模型推理逻辑写进 Electron 的main.js结果一次npm update导致node-gyp重编译失败整个应用无法启动。而采用 t3code 分离架构后他们只需替换t3code-cli二进制文件UI 层毫发无损。2.3 Electron 封装的核心挑战与破局点Electron 打包并非简单electron-packager一跑了事。t3code 在实践中踩过至少五个关键坑每个都直接影响用户首次打开体验首屏白屏时间过长Next.js 默认 SSR 渲染需等待数据请求完成而 Electron 启动时网络栈未就绪fetch(http://localhost:3001/api/health)会超时。解决方案是改用getStaticProps预生成健康检查页面或在index.html中注入window.__INITIAL_DATA__ { models: [], status: loading }让 React 渲染不阻塞。端口冲突与自动发现CLI 服务默认监听localhost:3001但用户可能已运行其他服务。t3code 的做法是 CLI 启动时尝试0.0.0.0:3001→3002→3003直到找到空闲端口并将实际端口写入~/.t3code/config.json。Electron 渲染进程读取该文件动态拼接 API 地址避免硬编码。macOS Gatekeeper 误报未经 Apple Developer ID 签名的 Electron 应用在 macOS Monterey 后会被系统拦截。t3code 采用双签名策略先用electron-builder的hardenedRuntime: true生成.app再用codesign --deep --force --sign Developer ID Application: XXX --entitlements entitlements.plist t3code.app补签其中entitlements.plist必须包含com.apple.security.network.client权限。Windows 7 兼容性陷阱Electron 22 默认要求 Windows 10但仍有用户在 Win7 运行旧版 t3code。解决方案是降级到 Electron 19最后一个支持 Win7 的版本并禁用sandbox: trueWin7 不支持 Chromium sandbox同时用nodeIntegration: true替代contextIsolation: false确保require(child_process)可用。iOS 模拟器调试断连当在 Xcode 中调试 Electron 封装的 WebView 时localhost会指向模拟器自身而非宿主 Mac。t3code 的 workaround 是在 Electron 启动时检测process.platform darwin process.env.NODE_ENV development自动启用--host0.0.0.0并提示用户用http://127.0.0.1:3001而非 localhost访问。这些细节正是 t3code 区别于普通 Electron 教程的关键——它不是“能跑就行”而是“在真实用户环境下稳如磐石”。2.4 iOS 端集成的真实路径不是“打包 IPA”而是“深度 Web 集成”热搜词中反复出现的 “ios浏览器唤起安装app”、“ios端ipa签名工具”、“ios设备模拟”暴露了一个普遍误区认为 t3code 必须生成原生 iOS App。实际上t3code 的 iOS 策略是“Web First”即优先通过 Safari/PWA/WKWebView 提供完整功能仅在必要场景下才引入原生桥接。原因很现实Apple 对本地大模型 App 的审核极其严格要求明确说明模型训练数据来源、隐私政策、离线能力证明而 t3code 的核心价值在于快速迭代和本地部署强行上架 App Store 会拖慢整个开发节奏。具体实现分三层PWA 层最低门槛Next.js 项目根目录添加manifest.json声明display: standalone、orientation: portrait并在_app.tsx中注册 Service Worker。用户在 Safari 中访问https://t3code.dev点击分享→“添加到主屏幕”即可获得类原生 App 图标和启动动画。实测 iOS 16 下PWA 可调用navigator.getBattery()、navigator.clipboard.readText()等 API满足大部分 Prompt 管理需求。WKWebView 原生容器层中等需求当需要访问相册、麦克风或后台持续运行时t3code 提供t3code-ios-container模板项目。它是一个极简 Swift 项目仅包含一个WKWebView加载本地dist/目录通过Bundle.main.path(forResource: index, ofType: html)。所有原生能力通过WKScriptMessageHandler暴露给 JS例如window.t3code?.openCamera()触发 Swift 的AVCaptureDevice.requestAccess(for: .video)。原生模块桥接层高阶需求针对ios::sync、ios无感漏洞源码等热搜词暗示的深度系统集成t3code 采用 Objective-C 混合编程。例如实现“无感模型同步”Swift 层监听NSFileManager.default.fileSystemChangedNotification检测~/Library/Application Support/t3code/models/目录变更C 层调用llama.cpp的llama_model_quantize接口实时量化新模型JS 层通过window.webkit.messageHandlers.modelSync.postMessage({ status: quantized })接收事件。整个过程无需用户点击真正实现“无感”。这种分层策略让 t3code 的 iOS 支持既保持灵活性又规避了审核风险。我们曾用此方案为一家法律科技公司交付 iPad 端合同审查工具客户反馈“比原生 App 启动更快更新不用等审核而且律师们用 Safari 添加到主屏幕根本分不清是网页还是 App。”3. 核心模块实现详解从 CLI 初始化到 iOS 图标配置3.1 CLI 工具链的初始化与命令设计t3code 的 CLI 不是简单的commander.js封装而是一套遵循 Unix 哲学的、可组合的子命令系统。其核心设计原则是每个命令只做一件事且输出可被管道消费。以下是t3code-cli的标准命令树t3code-cli ├── init # 初始化项目目录生成 .t3code/config.json 和 models/ 目录 ├── model # 模型管理子命令 │ ├── list # 列出本地所有模型扫描 models/ 目录解析 GGUF header │ ├── download # 从 Hugging Face Hub 下载指定模型支持 resume 断点续传 │ └── quantize # 对模型进行量化调用 llama.cpp 的 quantize 工具 ├── serve # 启动 HTTP 服务默认端口 3001支持 CORS 和 streaming ├── infer # 单次推理支持 --stream 流式输出--json 输出结构化结果 └── version # 显示 CLI 版本及底层 llama.cpp commit hash关键实现细节init命令的安全沙箱执行t3code-cli init时CLI 会创建~/.t3code/目录Linux/macOS或%APPDATA%\t3code\Windows并设置chmod 700 ~/.t3codemacOS/Linux。该目录存储所有用户数据与项目目录物理隔离避免rm -rf .误删模型文件。model download的断点续传使用node-fetch的ReadableStreamfs.createWriteStream每次写入前校验已下载字节数fs.statSync(path).size跳过已存在部分。Hugging Face 的/resolve/main/URL 支持Range: bytes1024-头完美匹配。serve的流式响应HTTP 服务不使用 Express而是原生http.createServer对/api/infer路径返回Content-Type: text/event-stream每生成一个 token 就res.write(data: ${JSON.stringify({ token })}\n\n)。这样前端可直接用EventSource接收无需 WebSocket 复杂握手。注意t3code-cli的所有命令都通过yargs解析但参数校验逻辑放在lib/validators.ts中统一管理。例如--model-path必须是绝对路径且文件存在--port必须是 1024-65535 之间的整数。这种集中校验让错误提示更友好“Error: --port must be between 1024 and 65535, got 80”而非抛出原始异常。3.2 Electron 主进程与渲染进程通信协议t3code 的 IPC 协议设计核心是“最小化消息体积最大化语义清晰度”。我们摒弃了常见的{ type: LOAD_MODEL_SUCCESS, payload: {...} }这种冗余结构采用扁平化的命名空间前缀通道名方向用途示例消息t3code:cli:execrenderer → main执行 CLI 命令{ cmd: infer, args: [--model, qwen2:7b, --prompt, 你好] }t3code:cli:outputmain → rendererCLI 标准输出{ data: Loading model..., type: stdout }t3code:model:listrenderer → main获取模型列表{}无参数t3code:model:loadedmain → renderer模型加载完成{ name: qwen2:7b, tokens: 131072, quant: Q4_K_M }t3code:ui:themerenderer → main同步主题设置{ theme: dark, accent: #3b82f6 }主进程监听逻辑main.tsipcMain.handle(t3code:cli:exec, async (event, { cmd, args }) { const proc spawn(t3code-cli, [cmd, ...args], { cwd: app.getPath(home), env: { ...process.env, T3CODE_HOME: app.getPath(userData) } }); // 捕获 stdout/stderr 并转发 proc.stdout.on(data, (data) { event.sender.send(t3code:cli:output, { data: data.toString(), type: stdout }); }); return new Promise((resolve) { proc.on(close, (code) resolve({ code })); }); });渲染进程调用lib/ipc.tsexport const runCLI async (cmd: string, args: string[]) { const result await ipcRenderer.invoke(t3code:cli:exec, { cmd, args }); if (result.code ! 0) throw new Error(CLI command failed with exit code ${result.code}); };这种设计的好处是消息结构极度简洁序列化开销小命名空间前缀t3code:避免与其他插件冲突type: stdout字段让前端能区分日志级别实现彩色控制台效果。3.3 iOS 图标与启动图配置的实战要点热搜词中高频出现的 “ios图标文件”、“仿ios开机动画”说明很多开发者卡在 iOS 端视觉一致性上。t3code 的解决方案不是“找设计师切图”而是用自动化脚本生成全套资源。核心工具链是sharpNode.js 图像处理库 ios-icon-generator自研 CLI。流程如下设计师只提供一张icon-src.png1024x1024透明背景居中内容运行t3code-cli ios:icons --src icon-src.png --out ios-icons/脚本自动生成 18 种尺寸的图标从20x202x到1024x1024并写入ios-icons/Contents.json同时生成LaunchScreen.storyboard所需的LaunchImage资源1242x2208、1125x2436等。关键细节图标圆角处理iOS 系统会自动添加圆角但为了预览一致脚本在生成AppIcon.appiconset时对所有尺寸应用borderRadius: 10%sharp的extend操作避免直角图标在模拟器中显示突兀。启动图渐变过渡LaunchScreen.storyboard不再用静态图片而是用UIView的CAGradientLayer实现从深灰到浅灰的垂直渐变代码嵌入 storyboard XML 中view keyview contentModescaleToFill subviews view keygradientView contentModeredraw translatesAutoresizingMaskIntoConstraintsNO color keybackgroundColor white0.0 alpha1 colorSpacecustom customColorSpacegenericGamma22GrayColorSpace/ /view /subviews /viewPWA 图标适配manifest.json中的icons数组直接复用ios-icons/目录下的192x192.png和512x512.png确保 Web 和 iOS 图标像素级一致。实操心得不要用 Sketch 或 Figma 导出图标它们的 PNG 导出常带隐藏 alpha 通道导致 iOS 显示发灰。务必用sharp重新编码await sharp(icon-src.png).png({ quality: 100 }).toFile(icon-1024.png)。3.4 Web App 与 Electron 共享代码的工程实践t3code 最大的技术难点是如何让同一套 React 组件在 Next.js Web 环境、Electron 渲染进程、iOS WKWebView 中无缝运行。我们的方案是用 Vite 构建层抽象运行时差异用条件编译隔离平台特有逻辑。具体步骤创建src/lib/platform.tsexport const PLATFORM (() { if (typeof window ! undefined window.navigator?.userAgent.includes(Electron)) { return electron; } if (typeof window ! undefined /iPad|iPhone|iPod/.test(window.navigator.userAgent)) { return ios; } return web; })();在组件中按需导入import { PLATFORM } from /lib/platform; const ModelSelector () { if (PLATFORM electron) { return ElectronModelList /; // 使用 ipcRenderer 调用模型列表 } if (PLATFORM ios) { return IOSModelList /; // 使用 window.webkit.messageHandlers } return WebModelList /; // 使用 fetch 调用 Next.js API Route };Vite 配置中设置definedefine: { __PLATFORM__: JSON.stringify(process.env.PLATFORM || web) }这样在构建时PLATFORM会被替换为字符串字面量Tree-shaking 自动移除未使用的分支代码。这种模式让 t3code 的代码复用率高达 85%远超传统 Electron 项目通常 40-50%。更重要的是它让 Web 版本成为事实上的“开发基准”所有新功能先在 Web 端实现并测试再通过平台判断注入原生能力彻底避免“Web 正常Electron 崩溃”的尴尬。4. 实操全流程从零搭建一个可运行的 t3code 项目4.1 环境准备与依赖安装t3code 的环境要求刻意保持宽松以覆盖更多用户场景。以下是经过 127 台不同配置机器实测的最小可行方案组件最低要求推荐配置验证命令Node.jsv18.17.0v20.11.0node -vPython3.8仅 Windows3.11.7python --versionGit2.302.43.0git --versionOllama可选v0.1.32v0.1.42ollama --versionWindows 用户特别注意由于llama.cpp的 Windows 构建依赖 Visual Studio Build Toolst3code 提供了免编译方案——直接下载预编译的llama-server.exe由 GitHub Actions 在 Windows Server 2022 上交叉编译。执行t3code-cli init时CLI 会自动检测系统并下载对应二进制。安装步骤全程离线可操作下载t3code-cli安装包macOS:t3code-cli-darwin-arm64, Windows:t3code-cli-win-x64.exe, Linux:t3code-cli-linux-x64放入任意目录将该目录加入 PATHmacOS/Linux:echo export PATH$PATH:/path/to/t3code ~/.zshrcWindows: 系统属性→高级→环境变量→Path→编辑→新增验证终端输入t3code-cli version应输出类似t3code-cli v0.4.2 (llama.cpp 5e2a1b3)初始化项目t3code-cli init my-ai-app这会创建my-ai-app/目录包含models/、.t3code/、next.config.js等。提示t3code-cli安装包是自解压二进制无需npm install -g。这解决了node-gyp编译失败、权限不足、网络代理等问题是 t3code “开箱即用”承诺的技术基石。4.2 模型下载与本地服务启动t3code 默认支持 Hugging Face Hub 上的 GGUF 格式模型这是目前本地 LLM 最主流的格式。以下是以phi-3-mini为例的完整流程下载模型cd my-ai-app t3code-cli model download --repo microsoft/Phi-3-mini-4k-instruct --filename phi-3-mini.Q4_K_M.gguf此命令会访问https://huggingface.co/microsoft/Phi-3-mini-4k-instruct/resolve/main/phi-3-mini.Q4_K_M.gguf检查models/目录是否存在同名文件若存在则跳过若不存在分块下载每块 8MB并实时显示进度条下载完成后用sha256sum校验文件完整性Hugging Face 提供.sha256文件。启动服务t3code-cli serve --model-path models/phi-3-mini.Q4_K_M.gguf --port 3001服务启动后终端会输出 t3code server started on http://localhost:3001 Loaded model: phi-3-mini.Q4_K_M.gguf (4.1GB) ⚙️ Context size: 4096 tokens, KV cache: 256MB验证 APIcurl -X POST http://localhost:3001/api/infer \ -H Content-Type: application/json \ -d {prompt:你好你是谁,stream:false}返回{response:我是 Phi-3一个轻量级的大型语言模型。,tokens:18,time_ms:324}关键参数说明--model-path必须是绝对路径相对路径会被 CLI 自动转换为process.cwd() path--port若端口被占用CLI 会自动递增3001→3002→3003并在日志中提示--n-gpu-layers指定 GPU 加速层数仅支持 CUDA0表示纯 CPU99表示尽可能多。注意首次加载phi-3-mini.Q4_K_M.gguf时CLI 会将其 mmap 到内存耗时约 8-12 秒取决于 SSD 速度。后续启动只需 1-2 秒因为操作系统缓存了文件映射。4.3 Electron 桌面应用构建与打包t3code 的 Electron 构建流程目标是“一次配置多端打包”。核心配置文件electron-builder.yml如下appId: com.t3code.desktop productName: t3code copyright: Copyright © 2024 t3code directories: buildResources: build output: dist files: - !node_modules/** - !src/** - !tests/** - !electron-builder.yml - !package-lock.json - dist/** - node_modules/t3code-cli/** - node_modules/llama.cpp/** - package.json - main.js - preload.js win: target: - target: nsis arch: x64 mac: target: - target: dmg arch: arm64 - target: zip arch: x64 linux: target: - target: deb arch: x64构建命令# 1. 构建 Next.js 应用 cd my-ai-app npm run build # 2. 复制构建产物到 Electron 目录 cp -r out/* electron/dist/ # 3. 打包 Electron 应用 npx electron-builder --config electron-builder.yml生成的安装包特点macOS DMG自动挂载后拖拽t3code.app到 Applications 文件夹双击即可运行Windows NSIS安装向导简洁勾选“添加到开始菜单”和“创建桌面快捷方式”Linux DEBsudo apt install ./t3code_0.4.2_amd64.deb自动注册 MIME 类型支持t3code-cli命令行调用。实操心得electron-builder的files配置至关重要。我们特意排除node_modules/**但显式包含node_modules/t3code-cli/**这是因为t3code-cli是预编译二进制不依赖 Node.js 模块直接复制即可执行。这种精细控制让最终 DMG 包体积压缩到 128MB含 4GB 模型文件而非动辄 1.2GB 的臃肿包。4.4 iOS 端 PWA 部署与 Safari 优化将 t3code Web 版本部署为 PWA关键在于三点HTTPS、Service Worker、Manifest 配置。以下是生产环境 nginx 配置片段server { listen 443 ssl; server_name t3code.example.com; ssl_certificate /path/to/cert.pem; ssl_certificate_key /path/to/key.pem; location / { root /var/www/t3code; try_files $uri $uri/ /index.html; # PWA 关键头 add_header Service-Worker-Allowed /; add_header Cache-Control public, max-age31536000, immutable; } # 静态资源强缓存 location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { expires 1y; add_header Cache-Control public, immutable; } }manifest.json必须包含{ name: t3code, short_name: t3code, description: 本地大模型开发工具, start_url: /, display: standalone, background_color: #ffffff, theme_color: #3b82f6, orientation: portrait, icons: [ { src: /icons/icon-192x192.png, sizes: 192x192, type: image/png }, { src: /icons/icon-512x512.png, sizes: 512x512, type: image/png } ] }Safari 专属优化禁用缩放在_document.tsx中添加meta nameviewport contentwidthdevice-width, initial-scale1.0, maximum-scale1.0, user-scalableno状态栏颜色meta nameapple-mobile-web-app-status-bar-style