diff --git a/website/i18n/en/code.json b/website/i18n/en/code.json index 2f6bc3878..e3e336f4c 100644 --- a/website/i18n/en/code.json +++ b/website/i18n/en/code.json @@ -8,6 +8,12 @@ "github": { "message": "GitHub" }, + "homepage.playground.hint": { + "message": "Every @ExcelProperty becomes a column header." + }, + "homepage.playground.sheet.caption": { + "message": "The spreadsheet Fesod writes from the class beside it" + }, "homepage.feature.quick.title": { "message": "High-performance" }, diff --git a/website/i18n/zh-cn/code.json b/website/i18n/zh-cn/code.json index e65d040dc..56b2866b6 100644 --- a/website/i18n/zh-cn/code.json +++ b/website/i18n/zh-cn/code.json @@ -8,6 +8,12 @@ "github": { "message": "GitHub" }, + "homepage.playground.hint": { + "message": "每个 @ExcelProperty 对应一个列标题。" + }, + "homepage.playground.sheet.caption": { + "message": "Fesod 依据左侧类写出的电子表格" + }, "homepage.feature.quick.title": { "message": "快速" }, diff --git a/website/src/components/HeroPlayground/index.js b/website/src/components/HeroPlayground/index.js new file mode 100644 index 000000000..9a419aa22 --- /dev/null +++ b/website/src/components/HeroPlayground/index.js @@ -0,0 +1,213 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import {useState} from 'react'; +import clsx from 'clsx'; +import Translate from '@docusaurus/Translate'; +import styles from './styles.module.css'; + +const FIELDS = [ + { + key: 'name', + type: 'String', + header: 'Name', + align: 'left', + values: ['Ada', 'Lin', 'Sun', 'Rui'], + }, + { + key: 'date', + type: 'Date', + header: 'Date', + align: 'right', + values: ['2026-07-28', '2026-07-28', '2026-07-29', '2026-07-30'], + }, + { + key: 'amount', + type: 'Double', + header: 'Amount', + align: 'right', + values: ['0.56', '1.20', '4.80', '2.35'], + }, + /* Annotated @ExcelIgnore, so it never reaches the sheet. */ + { + key: 'note', + type: 'String', + ignored: true, + }, +]; + +const COLUMNS = FIELDS.filter((field) => !field.ignored); +const ROW_COUNT = 4; + +/* Empty rows below the data, so the pane reads as a sheet rather than stopping short. */ +const FILLER_ROWS = 1; + +function columnLetter(index) { + return String.fromCharCode(65 + index); +} + +export default function HeroPlayground() { + const [activeKey, setActiveKey] = useState(null); + + const link = (key) => ({ + onMouseEnter: () => setActiveKey(key), + onMouseLeave: () => setActiveKey(null), + }); + + return ( +
+
+
+
DemoData.java
+
+                        
+                            class 
+                            DemoData {'{'}
+                            {FIELDS.map((field) => (
+                                
+                                    
+                                        {field.ignored ? (
+                                            @ExcelIgnore
+                                        ) : (
+                                            <>
+                                                
+                                                    @ExcelProperty
+                                                
+                                                {'("'}
+                                                
+                                                    {field.header}
+                                                
+                                                {'")'}
+                                            
+                                        )}
+                                    
+                                    
+                                        private 
+                                        {field.type} {field.key};
+                                    
+                                
+                            ))}
+                            {'}'}
+                        
+                    
+
+ +
+
demo.xlsx
+
+ + + + + + ))} + + + + {COLUMNS.map((field) => ( + + ))} + + + + {Array.from({length: ROW_COUNT}, (_, row) => ( + + + {COLUMNS.map((field) => ( + + ))} + + ))} + {Array.from({length: FILLER_ROWS}, (_, row) => ( + + + {COLUMNS.map((field) => ( + + ))} + + ))} + +
+ + The spreadsheet Fesod writes from the class beside it + +
+ {COLUMNS.map((field, index) => ( + + {columnLetter(index)} +
1 + {field.header} +
{row + 2} + {field.values[row]} +
+
+
Template
+
+
+ +
+ + FesodSheet.write("demo.xlsx", DemoData.class).sheet("Template").doWrite(data()); + +
+ +

+ + Every @ExcelProperty becomes a column header. + +

+
+ ); +} diff --git a/website/src/components/HeroPlayground/styles.module.css b/website/src/components/HeroPlayground/styles.module.css new file mode 100644 index 000000000..61372a6a3 --- /dev/null +++ b/website/src/components/HeroPlayground/styles.module.css @@ -0,0 +1,292 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/* + * The only chromatic value below is the Apache Fesod brand green, #14824b + * (R20 G130 B75). Every other value is white or black at some alpha, so the + * panel shades itself against the hero instead of introducing a second palette. + */ +.playground { + --pg-brand: #14824b; + --pg-brand-wash: rgba(20, 130, 75, 0.12); + --pg-brand-wash-strong: rgba(20, 130, 75, 0.18); + --pg-brand-tint: rgba(20, 130, 75, 0.45); + --pg-edge: rgba(255, 255, 255, 0.22); + --pg-rule: rgba(0, 0, 0, 0.13); + --pg-ink: rgba(0, 0, 0, 0.88); + --pg-ink-muted: rgba(0, 0, 0, 0.5); + --pg-gutter: rgba(0, 0, 0, 0.04); + + /* Infima stripes every second table row; a spreadsheet's cells are uniform, + and the stripes fight the column highlight. */ + --ifm-table-stripe-background: transparent; + + font-family: var(--ifm-font-family-monospace, monospace); + text-align: left; +} + +.panes { + display: flex; + align-items: stretch; + border: 1px solid var(--pg-edge); + border-bottom: none; + border-radius: 12px 12px 0 0; + overflow: hidden; + box-shadow: 0 24px 48px rgba(0, 0, 0, 0.32); +} + +/* Near-black rather than a translucent green, so the code stops competing + with the hero it sits on. */ +.codePane { + flex: 0 0 46%; + min-width: 0; + background: rgba(0, 0, 0, 0.72); +} + +.sheetPane { + display: flex; + flex: 1 1 auto; + flex-direction: column; + min-width: 0; + background: #ffffff; +} + +.paneLabel { + padding: 9px 14px; + font-size: 0.75rem; + letter-spacing: 0.02em; +} + +.codePane .paneLabel { + color: rgba(255, 255, 255, 0.6); + border-bottom: 1px solid rgba(255, 255, 255, 0.14); +} + +.sheetPane .paneLabel { + color: var(--pg-ink-muted); + border-bottom: 1px solid var(--pg-rule); +} + +/* Code pane ------------------------------------------------------------- */ + +.code { + margin: 0; + padding: 14px 16px 18px; + background: none; + border-radius: 0; + font-size: 0.875rem; + line-height: 1.7; + color: rgba(255, 255, 255, 0.92); + white-space: pre; + overflow-x: auto; +} + +.field { + display: block; + margin-top: 0.5em; + padding-left: 2.2ch; + border-radius: 4px; + transition: background-color 120ms ease; +} + +.fieldActive { + background: rgba(20, 130, 75, 0.34); +} + +.annotationLine, +.declaration { + display: block; +} + +.annotation { + color: #ffffff; +} + +.punctuation { + color: rgba(255, 255, 255, 0.58); +} + +.declaration { + color: rgba(255, 255, 255, 0.72); +} + +/* + * The header string carries the brand tint that its column header carries in + * the sheet, so the eye pairs the two without anything needing to be clicked. + * The negative margin cancels the padding, so the tint bleeds past the glyphs + * without pushing the quotes out and implying spaces inside the string. + */ +.headerLiteral { + padding: 0 2px; + margin: 0 -2px; + color: #ffffff; + background: var(--pg-brand-tint); + border-radius: 2px; + transition: background-color 120ms ease; +} + +.fieldActive .headerLiteral { + background: var(--pg-brand); +} + +/* Sheet pane ------------------------------------------------------------ */ + +.sheetScroll { + overflow-x: auto; +} + +/* Infima sets `table { display: block }` for scrolling; .sheetScroll does that + job here, and the cells need real table layout to fill the pane. */ +.sheet { + display: table; + width: 100%; + margin: 0; + font-size: 0.875rem; + color: var(--pg-ink); + border-collapse: collapse; + table-layout: auto; +} + +.sheet td, +.sheet th { + border: 1px solid var(--pg-rule); +} + +.corner, +.columnLetter, +.rowNumber { + padding: 4px 6px; + font-size: 0.6875rem; + font-weight: 400; + color: var(--pg-ink-muted); + text-align: center; + background: var(--pg-gutter); +} + +.rowNumber { + width: 2rem; +} + +.columnLetter { + transition: background-color 120ms ease, color 120ms ease; +} + +.columnLetterActive { + color: #ffffff; + background: var(--pg-brand); +} + +.headerCell { + padding: 10px 12px; + font-weight: 600; + white-space: nowrap; + background: var(--pg-brand-wash); + border-bottom: 2px solid var(--pg-brand); +} + +.cell { + padding: 9px 12px; + white-space: nowrap; + transition: background-color 120ms ease; +} + +.cellActive { + background: var(--pg-brand-wash-strong); +} + +.sheetTab { + margin-top: auto; + padding: 9px 14px; + font-size: 0.75rem; + color: var(--pg-brand); + border-top: 1px solid var(--pg-rule); +} + +.sheetTab::before { + content: ''; + display: inline-block; + width: 7px; + height: 7px; + margin-right: 7px; + vertical-align: baseline; + background: var(--pg-brand); + border-radius: 1px; +} + +/* Status bar and hint --------------------------------------------------- */ + +.statusBar { + padding: 11px 16px; + font-size: 0.8125rem; + color: rgba(255, 255, 255, 0.82); + background: rgba(0, 0, 0, 0.8); + border: 1px solid var(--pg-edge); + border-radius: 0 0 12px 12px; + overflow-x: auto; +} + +.statusBar code { + padding: 0; + color: inherit; + white-space: pre; + background: none; + border: none; +} + +.hint { + margin: 0.9rem 0 0; + font-size: 0.9375rem; + color: rgba(255, 255, 255, 0.82); +} + +.srOnly { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + +@media screen and (max-width: 996px) { + .playground { + max-width: 640px; + margin: 0 auto; + } + + .panes { + flex-direction: column; + } + + .codePane { + flex: 1 1 auto; + } +} + +@media (prefers-reduced-motion: reduce) { + .field, + .columnLetter, + .cell, + .headerLiteral { + transition: none; + } +} diff --git a/website/src/css/custom.css b/website/src/css/custom.css index 1285023a3..4167f5e34 100644 --- a/website/src/css/custom.css +++ b/website/src/css/custom.css @@ -52,7 +52,7 @@ } .hero { - background: radial-gradient(circle, #00b95f 20%, #69CD37 50%, #14824b 100%); + background: linear-gradient(135deg, rgba(0, 0, 0, 0.45) 0%, rgba(0, 0, 0, 0) 100%) #14824b; } .header-github-link::before { diff --git a/website/src/pages/index.js b/website/src/pages/index.js index cf1104f89..4eb86063e 100644 --- a/website/src/pages/index.js +++ b/website/src/pages/index.js @@ -22,39 +22,75 @@ import Link from '@docusaurus/Link'; import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; import Layout from '@theme/Layout'; import HomepageFeatures from '@site/src/components/HomepageFeatures'; +import HeroPlayground from '@site/src/components/HeroPlayground'; import Translate from '@docusaurus/Translate'; import Heading from '@theme/Heading'; import styles from './index.module.css'; +const GRID_SIZE = 48; +const COLUMN_COUNT = 50; +const ROW_COUNT = 14; + +function columnLetter(index) { + let n = index + 1; + let label = ''; + while (n > 0) { + const remainder = (n - 1) % 26; + label = String.fromCharCode(65 + remainder) + label; + n = Math.floor((n - 1) / 26); + } + return label; +} + function HomepageHeader() { const {siteConfig} = useDocusaurusContext(); return (
-
- - {siteConfig.title} - -

- site.description -

-
- - quickstart - +
diff --git a/website/src/pages/index.module.css b/website/src/pages/index.module.css index dead0f1c6..d51776de2 100644 --- a/website/src/pages/index.module.css +++ b/website/src/pages/index.module.css @@ -58,3 +58,95 @@ width: 20px; height: 20px; } + +.gridOverlay { + position: absolute; + inset: 0; + z-index: 0; + pointer-events: none; + background-image: + linear-gradient(rgba(255, 255, 255, 0.07) 1px, transparent 1px), + linear-gradient(90deg, rgba(255, 255, 255, 0.07) 1px, transparent 1px); + background-size: 48px 48px; + -webkit-mask-image: linear-gradient(to bottom, transparent, black 20%, black 80%, transparent); + mask-image: linear-gradient(to bottom, transparent, black 20%, black 80%, transparent); +} + +.colLabels, +.rowLabels { + position: absolute; + z-index: 0; + pointer-events: none; + font-family: var(--ifm-font-family-monospace, monospace); + font-size: 0.7rem; + color: rgba(255, 255, 255, 0.22); +} + +.colLabels { + top: 8px; + left: 0; + height: 16px; +} + +.colLabels span { + position: absolute; + top: 0; + text-align: center; +} + +.rowLabels { + top: 0; + left: 8px; + width: 16px; +} + +.rowLabels span { + position: absolute; + left: 0; + display: flex; + align-items: center; + justify-content: center; +} + +.heroContent { + position: relative; + z-index: 2; +} + +.heroDemo { + margin-top: 2.5rem; +} + +@media screen and (min-width: 997px) { + .heroBanner { + padding: 4.5rem 0; + } + + /* The demo is the densest thing on the page, so the hero opts out of the + 1140px content width the rest of the page uses and gives it room. */ + .heroContent { + --ifm-container-width: 1280px; + --ifm-container-width-xl: 1280px; + + display: grid; + grid-template-columns: minmax(0, 4.5fr) minmax(0, 7.5fr); + gap: 3rem; + align-items: center; + text-align: left; + } + + .heroCopy .buttons { + justify-content: flex-start; + } + + .heroDemo { + margin-top: 0; + } +} + +@media screen and (max-width: 996px) { + .colLabels, + .rowLabels { + display: none; + } +}