feat: Add browser compatibility detection and fallback page (#11309)

*  feat: Add browser compatibility detection and fallback page

- Add automatic browser compatibility check in app layout
- Create standalone not-compatible.html fallback page with modern responsive design
- Support dark mode via prefers-color-scheme
- Include browser download links (Chrome, Firefox, Edge, Safari, Arc)
- Display minimum browser requirements
- Update feature development documentation

* 📝 docs(CLAUDE): Update PR Linear Issue Association guidelines

- Clarify the requirement to include magic keywords in PR body for Linear issues.
- Add instruction to summarize work done in the Linear issue comment and update the issue status to "In Review".

Signed-off-by: Innei <tukon479@gmail.com>

*  feat: Update browser compatibility page and layout

- Change favicon link to absolute path in not-compatible.html.
- Add Safari browser support with corresponding icon and link.
- Update minimum browser requirements to Chrome 99+, Safari 16.4+, and Edge 99+.
- Fix typo in layout.tsx comments from "serveral" to "several".

Signed-off-by: Innei <tukon479@gmail.com>

---------

Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
Innei
2026-01-07 23:00:52 +08:00
committed by GitHub
parent a4003a383b
commit 8be32c2f32
5 changed files with 1355 additions and 12 deletions

View File

@@ -80,7 +80,7 @@ When creating new Linear issues using `mcp__linear-server__create_issue`, **MUST
### PR Linear Issue Association (REQUIRED)
**When creating PRs for Linear issues, MUST include magic keywords in PR body:** `Fixes LOBE-123`, `Closes LOBE-123`, or `Resolves LOBE-123`
**When creating PRs for Linear issues, MUST include magic keywords in PR body:** `Fixes LOBE-123`, `Closes LOBE-123`, or `Resolves LOBE-123`, and summarize the work done in the linear issue comment and update the issue status to "In Review".
### IMPORTANT: Per-Issue Completion Rule

View File

@@ -254,14 +254,14 @@ Let's take the subcomponent `OpeningQuestion.tsx` as an example. Component imple
import { DeleteOutlined, PlusOutlined } from '@ant-design/icons';
import { Flexbox, SortableList } from '@lobehub/ui';
import { Button, Empty, Input } from 'antd';
import { createStyles } from 'antd-style';
import { createStaticStyles } from 'antd-style';
import { memo, useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useStore } from '../store';
import { selectors } from '../store/selectors';
const useStyles = createStyles(({ css, token }) => ({
const styles = createStaticStyles(({ css, cssVar }) => ({
empty: css`
margin-block: 24px;
margin-inline: 0;
@@ -270,7 +270,7 @@ const useStyles = createStyles(({ css, token }) => ({
margin-block-end: 8px;
padding-block: 2px;
padding-inline: 10px 0;
background: ${token.colorBgContainer};
background: ${cssVar.colorBgContainer};
`,
questionItemContent: css`
flex: 1;
@@ -281,7 +281,7 @@ const useStyles = createStyles(({ css, token }) => ({
`,
repeatError: css`
margin: 0;
color: ${token.colorErrorText};
color: ${cssVar.colorErrorText};
`,
}));
@@ -292,7 +292,6 @@ interface QuestionItem {
const OpeningQuestions = memo(() => {
const { t } = useTranslation('setting');
const { styles } = useStyles();
const [questionInput, setQuestionInput] = useState('');
// Use selector to access corresponding configuration

View File

@@ -254,14 +254,14 @@ lobe-chat 是个国际化项目,新加的文案需要更新默认的 `locale`
import { DeleteOutlined, PlusOutlined } from '@ant-design/icons';
import { Flexbox, SortableList } from '@lobehub/ui';
import { Button, Empty, Input } from 'antd';
import { createStyles } from 'antd-style';
import { createStaticStyles } from 'antd-style';
import { memo, useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useStore } from '../store';
import { selectors } from '../store/selectors';
const useStyles = createStyles(({ css, token }) => ({
const styles = createStaticStyles(({ css, cssVar }) => ({
empty: css`
margin-block: 24px;
margin-inline: 0;
@@ -270,7 +270,7 @@ const useStyles = createStyles(({ css, token }) => ({
margin-block-end: 8px;
padding-block: 2px;
padding-inline: 10px 0;
background: ${token.colorBgContainer};
background: ${cssVar.colorBgContainer};
`,
questionItemContent: css`
flex: 1;
@@ -281,7 +281,7 @@ const useStyles = createStyles(({ css, token }) => ({
`,
repeatError: css`
margin: 0;
color: ${token.colorErrorText};
color: ${cssVar.colorErrorText};
`,
}));
@@ -292,7 +292,6 @@ interface QuestionItem {
const OpeningQuestions = memo(() => {
const { t } = useTranslation('setting');
const { styles } = useStyles();
const [questionInput, setQuestionInput] = useState('');
// 使用 selector 访问对应配置

1296
public/not-compatible.html Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -50,6 +50,9 @@ const RootLayout = async ({ children, params }: RootLayoutProps) => {
return (
<html dir={direction} lang={locale} suppressHydrationWarning>
<head>
{/* eslint-disable-next-line @typescript-eslint/no-use-before-define */}
<script dangerouslySetInnerHTML={{ __html: `(${outdateBrowserScript.toString()})();` }} />
{/* <script dangerouslySetInnerHTML={{ __html: 'setTimeout(() => {debugger}, 16)' }} /> */}
{process.env.DEBUG_REACT_SCAN === '1' && (
<Script
@@ -74,6 +77,52 @@ const RootLayout = async ({ children, params }: RootLayoutProps) => {
);
};
function outdateBrowserScript() {
// eslint-disable-next-line unicorn/consistent-function-scoping
function supportsImportMaps(): boolean {
return (
typeof HTMLScriptElement !== 'undefined' &&
typeof (HTMLScriptElement as any).supports === 'function' &&
(HTMLScriptElement as any).supports('importmap')
);
}
// eslint-disable-next-line unicorn/consistent-function-scoping
function supportsCascadeLayers(): boolean {
if (typeof document === 'undefined') return false;
const el = document.createElement('div');
el.className = '__layer_test__';
el.style.position = 'absolute';
el.style.left = '-99999px';
el.style.top = '-99999px';
const style = document.createElement('style');
style.textContent = `
@layer a, b;
@layer a { .__layer_test__ { color: rgb(1, 2, 3); } }
@layer b { .__layer_test__ { color: rgb(4, 5, 6); } }
`;
document.documentElement.append(style);
document.documentElement.append(el);
const color = getComputedStyle(el).color;
el.remove();
style.remove();
return color === 'rgb(4, 5, 6)';
}
const isOutdateBrowser = !(supportsImportMaps() && supportsCascadeLayers());
if (isOutdateBrowser) {
window.location.href = '/not-compatible.html';
return true;
}
return false;
}
export default RootLayout;
export { generateMetadata } from './metadata';
@@ -99,7 +148,7 @@ export const generateViewport = async (props: DynamicLayoutProps): ResolvingView
export const generateStaticParams = () => {
const mobileOptions = isDesktop ? [false] : [true, false];
// only static for serveral page, other go to dynamtic
// only static for several page, other go to dynamic
const staticLocales: Locales[] = [DEFAULT_LANG, 'zh-CN'];
const variants: { variants: string }[] = [];