├── .eslintignore ├── .eslintrc.js ├── .gitattributes ├── .github ├── FUNDING.yml └── workflows │ ├── deploy.yml │ ├── main.yml │ └── publish.yml ├── .gitignore ├── .prettierignore ├── .prettierrc.js ├── .storybook ├── main.js ├── manager.js ├── preview-head.html ├── preview.js ├── style.css └── theme.js ├── LICENSE ├── README.md ├── bin └── luna.js ├── index.json ├── lib ├── build.js ├── doc.js └── util.js ├── package.json ├── public ├── Contra.nes ├── Get_along.jpg ├── Get_along.mp3 ├── Give_a_reason.jpg ├── Give_a_reason.mp3 ├── browserfs.min.js ├── favicon.ico ├── fceumm_libretro.js ├── fceumm_libretro.wasm ├── icon.png ├── logo.png ├── pic1.png ├── pic2.png ├── pic3.png ├── pic4.png ├── snes9x_libretro.js ├── snes9x_libretro.wasm ├── vba_next_libretro.js ├── vba_next_libretro.wasm └── wallpaper.png ├── src ├── box-model │ ├── CHANGELOG.md │ ├── README.md │ ├── index.ts │ ├── package.json │ ├── story.js │ ├── style.scss │ └── test.js ├── carousel │ ├── README.md │ ├── icon.css │ ├── icon.json │ ├── index.ts │ ├── package.json │ ├── story.js │ ├── style.scss │ └── test.js ├── chart │ ├── BarChart.ts │ ├── BaseChart.ts │ ├── LineChart.ts │ ├── PieChart.ts │ ├── README.md │ ├── RingChart.ts │ ├── index.ts │ ├── package.json │ ├── story.js │ ├── test.js │ └── util.ts ├── color-picker │ ├── README.md │ ├── index.ts │ ├── package.json │ ├── story.js │ ├── style.scss │ └── util.ts ├── command-palette │ ├── README.md │ ├── index.ts │ ├── package.json │ ├── react.tsx │ ├── story.js │ ├── style.scss │ └── test.js ├── console │ ├── CHANGELOG.md │ ├── Log.ts │ ├── README.md │ ├── getPreview.ts │ ├── icon.css │ ├── icon.json │ ├── index.ts │ ├── package.json │ ├── story.js │ ├── style.scss │ ├── test.js │ └── util.ts ├── cropper │ ├── README.md │ ├── index.ts │ ├── package.json │ ├── react.tsx │ ├── story.js │ ├── style.scss │ └── test.js ├── danmaku │ ├── README.md │ ├── index.ts │ ├── package.json │ ├── story.js │ └── style.scss ├── data-grid │ ├── CHANGELOG.md │ ├── README.md │ ├── icon.css │ ├── icon.json │ ├── index.ts │ ├── package.json │ ├── react.tsx │ ├── story.js │ ├── style.scss │ └── test.js ├── dom-highlighter │ ├── CHANGELOG.md │ ├── README.md │ ├── elementRoles.ts │ ├── index.ts │ ├── overlay │ │ ├── ColorUtils.ts │ │ ├── common.ts │ │ ├── css_grid_label_helpers.ts │ │ ├── highlight_common.ts │ │ └── tool_highlight.ts │ ├── package.json │ ├── story.js │ ├── style.scss │ └── test.js ├── dom-viewer │ ├── CHANGELOG.md │ ├── README.md │ ├── icon.css │ ├── icon.json │ ├── index.ts │ ├── package.json │ ├── react.tsx │ ├── story.js │ ├── style.scss │ └── test.js ├── drag-selector │ ├── README.md │ ├── index.ts │ ├── package.json │ ├── story.js │ ├── style.scss │ └── test.js ├── editor │ ├── README.md │ ├── Selection.ts │ ├── Toolbar.ts │ ├── icon.css │ ├── icon.json │ ├── index.ts │ ├── package.json │ ├── story.js │ ├── style.scss │ └── test.js ├── file-list │ ├── README.md │ ├── asset.ts │ ├── asset │ │ ├── audio.svg │ │ ├── file.svg │ │ ├── folder.svg │ │ ├── image.svg │ │ ├── text.svg │ │ └── video.svg │ ├── index.ts │ ├── package.json │ ├── react.tsx │ ├── story.js │ ├── style.scss │ └── test.js ├── gallery │ ├── CHANGELOG.md │ ├── README.md │ ├── icon.css │ ├── icon.json │ ├── index.ts │ ├── package.json │ ├── story.js │ ├── style.scss │ └── test.js ├── icon-list │ ├── README.md │ ├── index.ts │ ├── package.json │ ├── react.tsx │ ├── story.js │ ├── style.scss │ └── test.js ├── image-list │ ├── README.md │ ├── index.ts │ ├── package.json │ ├── story.js │ ├── style.scss │ ├── test.js │ └── vue.ts ├── image-viewer │ ├── CHANGELOG.md │ ├── README.md │ ├── index.ts │ ├── package.json │ ├── react.tsx │ ├── story.js │ ├── style.scss │ └── test.js ├── json-editor │ ├── README.md │ ├── icon.css │ ├── icon.json │ ├── index.ts │ ├── package.json │ ├── story.js │ ├── style.scss │ └── test.js ├── keyboard │ ├── README.md │ ├── index.ts │ ├── package.json │ ├── story.js │ ├── style.scss │ └── test.js ├── log │ ├── README.md │ ├── ansiToHtml.ts │ ├── build.log │ ├── index.ts │ ├── package.json │ ├── react.tsx │ ├── story.js │ └── test.js ├── logcat │ ├── README.md │ ├── index.ts │ ├── logcat.json │ ├── package.json │ ├── react.tsx │ ├── story.js │ ├── style.scss │ └── test.js ├── lrc-player │ ├── README.md │ ├── index.ts │ ├── package.json │ ├── story.js │ └── style.scss ├── markdown-editor │ ├── README.md │ ├── icon.css │ ├── icon.json │ ├── index.ts │ ├── package.json │ ├── story.js │ ├── style.scss │ └── test.js ├── markdown-viewer │ ├── DEMO.md │ ├── README.md │ ├── index.ts │ ├── package.json │ ├── story.js │ ├── style.scss │ └── test.js ├── mask-editor │ ├── README.md │ ├── index.ts │ ├── package.json │ ├── react.tsx │ ├── story.js │ └── test.js ├── menu-bar │ ├── README.md │ ├── index.ts │ ├── package.json │ ├── story.js │ ├── style.scss │ └── test.js ├── menu │ ├── README.md │ ├── icon.css │ ├── icon.json │ ├── index.ts │ ├── package.json │ ├── story.js │ ├── style.scss │ └── test.js ├── modal │ ├── CHANGELOG.md │ ├── README.md │ ├── icon.css │ ├── icon.json │ ├── index.ts │ ├── package.json │ ├── react.tsx │ ├── story.js │ ├── style.scss │ └── test.js ├── music-player │ ├── README.md │ ├── icon.css │ ├── icon.json │ ├── index.ts │ ├── package.json │ ├── story.js │ ├── style.scss │ ├── test.js │ └── util.ts ├── music-visualizer │ ├── BarEffect.ts │ ├── CircleEffect.ts │ ├── LineEffect.ts │ ├── README.md │ ├── icon.css │ ├── icon.json │ ├── index.ts │ ├── package.json │ ├── story.js │ ├── style.scss │ └── test.js ├── notification │ ├── README.md │ ├── icon.css │ ├── icon.json │ ├── index.ts │ ├── package.json │ ├── story.js │ ├── style.scss │ └── test.js ├── object-viewer │ ├── README.md │ ├── Static.ts │ ├── Visitor.ts │ ├── icon.css │ ├── icon.json │ ├── index.ts │ ├── package.json │ ├── react.tsx │ ├── story.js │ ├── style.scss │ ├── test.js │ └── util.ts ├── otp-input │ ├── README.md │ ├── index.ts │ ├── package.json │ ├── react.tsx │ ├── story.js │ ├── style.scss │ └── test.js ├── painter │ ├── README.md │ ├── icon.css │ ├── icon.json │ ├── index.ts │ ├── package.json │ ├── react.tsx │ ├── story.js │ ├── style.scss │ ├── test.js │ ├── tools │ │ ├── Brush.ts │ │ ├── Eraser.ts │ │ ├── Eyedropper.ts │ │ ├── Hand.ts │ │ ├── PaintBucket.ts │ │ ├── Pencil.ts │ │ ├── Tool.ts │ │ ├── Zoom.ts │ │ └── index.ts │ └── util.ts ├── performance-monitor │ ├── CHANGELOG.md │ ├── README.md │ ├── index.ts │ ├── package.json │ ├── react.tsx │ ├── story.js │ ├── style.scss │ └── test.js ├── qrcode-generator │ ├── README.md │ ├── index.ts │ ├── package.json │ ├── story.js │ └── style.scss ├── retro-emulator │ ├── README.md │ ├── bootstrap.js │ ├── icon.css │ ├── icon.json │ ├── index.ts │ ├── package.json │ ├── story.js │ ├── style.scss │ └── test.js ├── retro-handheld │ ├── README.md │ ├── index.ts │ ├── package.json │ ├── story.js │ ├── style.scss │ └── test.js ├── scrollbar │ ├── README.md │ ├── index.ts │ ├── package.json │ ├── story.js │ ├── style.scss │ └── test.js ├── setting │ ├── CHANGELOG.md │ ├── README.md │ ├── index.ts │ ├── package.json │ ├── react.tsx │ ├── story.js │ ├── style.scss │ ├── test.js │ └── util.ts ├── shader-toy-player │ ├── Effect.ts │ ├── README.md │ ├── icon.css │ ├── icon.json │ ├── index.ts │ ├── package.json │ ├── piLibs.ts │ ├── shaders.js │ ├── story.js │ ├── style.scss │ ├── test.js │ └── vue.ts ├── share │ ├── Component.ts │ ├── hooks.ts │ ├── icon │ │ ├── add.svg │ │ ├── arrow-left.svg │ │ ├── arrow-right.svg │ │ ├── bold.svg │ │ ├── brush.svg │ │ ├── camera.svg │ │ ├── caret-down.svg │ │ ├── caret-right.svg │ │ ├── caret-up.svg │ │ ├── check.svg │ │ ├── close.svg │ │ ├── copy.svg │ │ ├── crosshair.svg │ │ ├── delete.svg │ │ ├── download.svg │ │ ├── eraser.svg │ │ ├── error.svg │ │ ├── eye.svg │ │ ├── eyedropper.svg │ │ ├── file.svg │ │ ├── fullscreen.svg │ │ ├── hand.svg │ │ ├── header.svg │ │ ├── horizontal-rule.svg │ │ ├── info.svg │ │ ├── input.svg │ │ ├── italic.svg │ │ ├── list.svg │ │ ├── loop-all.svg │ │ ├── loop-off.svg │ │ ├── loop-one.svg │ │ ├── maximize.svg │ │ ├── maximized.svg │ │ ├── minimize.svg │ │ ├── original.svg │ │ ├── output.svg │ │ ├── paint-bucket.svg │ │ ├── pause.svg │ │ ├── pencil.svg │ │ ├── pip.svg │ │ ├── play.svg │ │ ├── quote.svg │ │ ├── reset-color.svg │ │ ├── shuffle-disabled.svg │ │ ├── shuffle.svg │ │ ├── step-backward.svg │ │ ├── step-forward.svg │ │ ├── strike-through.svg │ │ ├── swap.svg │ │ ├── underline.svg │ │ ├── volume-down.svg │ │ ├── volume-off.svg │ │ ├── volume.svg │ │ ├── warn.svg │ │ ├── zoom-in.svg │ │ ├── zoom-out.svg │ │ └── zoom.svg │ ├── karma.conf.js │ ├── mixin.scss │ ├── react.tsx │ ├── story.js │ ├── test.js │ ├── theme.js │ ├── theme.json │ ├── theme.scss │ ├── types.ts │ ├── util.ts │ └── webpack.config.js ├── syntax-highlighter │ ├── CHANGELOG.md │ ├── README.md │ ├── index.ts │ ├── package.json │ ├── story.js │ ├── style.scss │ └── test.js ├── tab │ ├── README.md │ ├── icon.css │ ├── icon.json │ ├── index.ts │ ├── package.json │ ├── react.tsx │ ├── story.js │ ├── style.scss │ └── test.js ├── tag-input │ ├── README.md │ ├── index.ts │ ├── package.json │ ├── story.js │ └── style.scss ├── text-viewer │ ├── README.md │ ├── icon.css │ ├── icon.json │ ├── index.ts │ ├── package.json │ ├── story.js │ ├── style.scss │ └── test.js ├── toolbar │ ├── README.md │ ├── index.ts │ ├── package.json │ ├── react.tsx │ ├── story.js │ ├── style.scss │ └── test.js ├── video-player │ ├── CHANGELOG.md │ ├── README.md │ ├── icon.css │ ├── icon.json │ ├── index.ts │ ├── package.json │ ├── story.js │ ├── style.scss │ └── test.js ├── virtual-list │ ├── README.md │ ├── index.ts │ ├── package.json │ ├── story.js │ ├── style.scss │ └── test.js └── window │ ├── README.md │ ├── icon.css │ ├── icon.json │ ├── index.ts │ ├── package.json │ ├── story.js │ ├── style.scss │ └── test.js └── tsconfig.json /.eslintignore: -------------------------------------------------------------------------------- 1 | src/dom-highlighter/overlay/ -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | browser: true, 5 | node: true, 6 | }, 7 | parser: '@typescript-eslint/parser', 8 | plugins: ['@typescript-eslint'], 9 | extends: [ 10 | 'eslint:recommended', 11 | 'plugin:@typescript-eslint/recommended', 12 | 'prettier', 13 | 'prettier/@typescript-eslint', 14 | ], 15 | rules: { 16 | '@typescript-eslint/no-explicit-any': 'off', 17 | '@typescript-eslint/explicit-module-boundary-types': 'off', 18 | '@typescript-eslint/no-this-alias': 'off', 19 | '@typescript-eslint/ban-types': 'off', 20 | '@typescript-eslint/ban-ts-comment': 'off', 21 | '@typescript-eslint/no-non-null-assertion': 'off', 22 | }, 23 | overrides: [ 24 | { 25 | files: ['**/*.js'], 26 | rules: { 27 | '@typescript-eslint/no-var-requires': 'off', 28 | }, 29 | }, 30 | ], 31 | } 32 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.jpg filter=lfs diff=lfs merge=lfs -text 2 | *.wasm filter=lfs diff=lfs merge=lfs -text 3 | *.mp3 filter=lfs diff=lfs merge=lfs -text 4 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | ko_fi: surunzi 2 | custom: [surunzi.com/wechatpay.html] -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Luna 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | deploy: 8 | 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v4 13 | with: 14 | path: project/luna 15 | lfs: true 16 | - uses: actions/setup-node@v4 17 | with: 18 | node-version: '18.x' 19 | - working-directory: ./project/luna 20 | run: | 21 | npm i 22 | npm link 23 | luna install 24 | npm run build 25 | npm run build:storybook 26 | mkdir -p ../../page/luna 27 | cp -r .storybook/out/* ../../page/luna 28 | - name: Copy file via ssh password 29 | uses: appleboy/scp-action@master 30 | with: 31 | host: ${{ secrets.HOST }} 32 | username: ${{ secrets.USERNAME }} 33 | password: ${{ secrets.PASSWORD }} 34 | source: "page/luna/" 35 | target: "/root/" 36 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - 'master' 8 | paths: 9 | - 'src/**/*' 10 | - 'lib/**/*' 11 | 12 | jobs: 13 | ci: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | - uses: actions/setup-node@v4 20 | with: 21 | node-version: '18.x' 22 | - run: | 23 | npm i 24 | npm link 25 | npm run ci 26 | - uses: codecov/codecov-action@v4 27 | with: 28 | token: ${{ secrets.CODECOV_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish to NPM 2 | 3 | run-name: Publish ${{ github.event.inputs.component }} to NPM 4 | 5 | on: 6 | workflow_dispatch: 7 | inputs: 8 | component: 9 | description: 'Component' 10 | required: true 11 | 12 | jobs: 13 | publish: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | - uses: actions/setup-node@v4 20 | with: 21 | node-version: '18.x' 22 | registry-url: 'https://registry.npmjs.org' 23 | - run: | 24 | npm i 25 | npm link 26 | luna install 27 | luna build ${{ github.event.inputs.component }} 28 | - working-directory: dist 29 | run: | 30 | cd ${{ github.event.inputs.component }} 31 | npm publish 32 | env: 33 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | coverage/ 4 | .storybook/out/ 5 | package-lock.json 6 | webpack.config.js 7 | karma.conf.js 8 | typedoc.json 9 | tsconfig.json -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | src/dom-highlighter/overlay/ -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | singleQuote: true, 3 | tabWidth: 2, 4 | semi: false, 5 | printWidth: 80, 6 | } 7 | -------------------------------------------------------------------------------- /.storybook/manager.js: -------------------------------------------------------------------------------- 1 | import { addons } from '@storybook/addons' 2 | import theme from './theme' 3 | import loadJs from 'licia/loadJs' 4 | import './style.css' 5 | 6 | addons.setConfig({ 7 | theme, 8 | panelPosition: 'right', 9 | enableShortcuts: false, 10 | }) 11 | 12 | loadJs( 13 | 'https://www.googletagmanager.com/gtag/js?id=G-26RRF9531G', 14 | (isLoaded) => { 15 | if (isLoaded) { 16 | window.dataLayer = window.dataLayer || [] 17 | function gtag() { 18 | dataLayer.push(arguments) 19 | } 20 | gtag('js', new Date()) 21 | 22 | gtag('config', 'G-26RRF9531G') 23 | } 24 | } 25 | ) 26 | -------------------------------------------------------------------------------- /.storybook/preview.js: -------------------------------------------------------------------------------- 1 | import { addParameters } from '@storybook/html' 2 | 3 | addParameters({ 4 | options: {}, 5 | }) 6 | -------------------------------------------------------------------------------- /.storybook/style.css: -------------------------------------------------------------------------------- 1 | .sidebar-header > a { 2 | display: none; 3 | } 4 | 5 | [role='main'] > div { 6 | box-shadow: 0 1px 2px 0 rgba(60, 64, 67, 0.3), 7 | 0 2px 6px 2px rgba(60, 64, 67, 0.15); 8 | } 9 | -------------------------------------------------------------------------------- /.storybook/theme.js: -------------------------------------------------------------------------------- 1 | import { create } from '@storybook/theming/create' 2 | 3 | export default create({ 4 | base: 'light', 5 | brandUrl: 'https://github.com/liriliri/luna', 6 | brandImage: 'icon.png', 7 | brandTitle: 'LUNA UI', 8 | colorSecondary: '#f8866e', 9 | appBg: '#f6f9fc', 10 | appContentBg: '#FFF', 11 | appBorderColor: '#d9d9d9', 12 | appBorderRadius: 0, 13 | }) 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020-present liriliri 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /public/Contra.nes: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liriliri/luna/09cfb448f0e8646fb790feb6ca03e6799e5dc697/public/Contra.nes -------------------------------------------------------------------------------- /public/Get_along.jpg: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:ee89d3d42f5bbe15f81ca9d8af9f6aad14074391f6142418378fa1e84124f866 3 | size 63395 4 | -------------------------------------------------------------------------------- /public/Get_along.mp3: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:e00ed53c84b7170d2bf620cf9112b159aafcb9248d9ae14efed1944fcb3ccecc 3 | size 1525177 4 | -------------------------------------------------------------------------------- /public/Give_a_reason.jpg: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:fa873fe2c312aa596fa44f525378ee7842452efff7219edc537c316f9c4835a3 3 | size 14761 4 | -------------------------------------------------------------------------------- /public/Give_a_reason.mp3: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:e46e82530e830a9e9a9afac0a7f3945b61e88ecdfbda3188f8162864fde8dd95 3 | size 1465408 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liriliri/luna/09cfb448f0e8646fb790feb6ca03e6799e5dc697/public/favicon.ico -------------------------------------------------------------------------------- /public/fceumm_libretro.wasm: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:aa022e70dee17d03cbd1dcf3415f1db9b27f384d135f4273922fd30a3420eaf7 3 | size 2564774 4 | -------------------------------------------------------------------------------- /public/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liriliri/luna/09cfb448f0e8646fb790feb6ca03e6799e5dc697/public/icon.png -------------------------------------------------------------------------------- /public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liriliri/luna/09cfb448f0e8646fb790feb6ca03e6799e5dc697/public/logo.png -------------------------------------------------------------------------------- /public/pic1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liriliri/luna/09cfb448f0e8646fb790feb6ca03e6799e5dc697/public/pic1.png -------------------------------------------------------------------------------- /public/pic2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liriliri/luna/09cfb448f0e8646fb790feb6ca03e6799e5dc697/public/pic2.png -------------------------------------------------------------------------------- /public/pic3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liriliri/luna/09cfb448f0e8646fb790feb6ca03e6799e5dc697/public/pic3.png -------------------------------------------------------------------------------- /public/pic4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liriliri/luna/09cfb448f0e8646fb790feb6ca03e6799e5dc697/public/pic4.png -------------------------------------------------------------------------------- /public/snes9x_libretro.wasm: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:08c740ee66003c14ec204658af0cacab695e04e929d85cc08b6843afe8376f2a 3 | size 4272891 4 | -------------------------------------------------------------------------------- /public/vba_next_libretro.wasm: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:8b8867b0e1596be0951a3178f08eb17ef722054019bf81bc7e92ca266a0bfa71 3 | size 2658488 4 | -------------------------------------------------------------------------------- /public/wallpaper.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liriliri/luna/09cfb448f0e8646fb790feb6ca03e6799e5dc697/public/wallpaper.png -------------------------------------------------------------------------------- /src/box-model/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.0.0 (6 Sep 2024) 2 | 3 | * feat: hover effect -------------------------------------------------------------------------------- /src/box-model/README.md: -------------------------------------------------------------------------------- 1 | # Luna Box Model 2 | 3 | Css box model metrics. 4 | 5 | ## Demo 6 | 7 | https://luna.liriliri.io/?path=/story/box-model 8 | 9 | ## Install 10 | 11 | Add the following script and style to your page. 12 | 13 | ```html 14 | 15 | 16 | ``` 17 | 18 | You can also get it on npm. 19 | 20 | ```bash 21 | npm install luna-box-model --save 22 | ``` 23 | 24 | ```javascript 25 | import 'luna-box-model/luna-box-model.css' 26 | import LunaBoxModel from 'luna-box-model' 27 | ``` 28 | 29 | ## Usage 30 | 31 | ```javascript 32 | const boxModel = new LunaBoxModel(container) 33 | boxModel.setOption('element', document.getElementById('target')) 34 | ``` 35 | 36 | ## Configuration 37 | 38 | * element(HTMLElement): Target element. 39 | -------------------------------------------------------------------------------- /src/box-model/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "box-model", 3 | "version": "1.0.0", 4 | "description": "Css box model metrics" 5 | } 6 | -------------------------------------------------------------------------------- /src/box-model/test.js: -------------------------------------------------------------------------------- 1 | import BoxModel from './index' 2 | import test from '../share/test' 3 | 4 | test('box-model', (container) => { 5 | const boxModel = new BoxModel(container) 6 | it('basic', function () { 7 | boxModel.setOption('element', document.body) 8 | }) 9 | }) 10 | -------------------------------------------------------------------------------- /src/carousel/icon.json: -------------------------------------------------------------------------------- 1 | ["arrow-left.svg", "arrow-right.svg"] 2 | -------------------------------------------------------------------------------- /src/carousel/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "carousel", 3 | "version": "0.3.0", 4 | "description": "Lightweight carousel", 5 | "luna": { 6 | "icon": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/carousel/story.js: -------------------------------------------------------------------------------- 1 | import 'luna-carousel.css' 2 | import Carousel from 'luna-carousel.js' 3 | import $ from 'licia/$' 4 | import story from '../share/story' 5 | import readme from './README.md' 6 | import { number, button } from '@storybook/addon-knobs' 7 | 8 | const def = story( 9 | 'carousel', 10 | (container) => { 11 | $(container).css({ 12 | width: '100%', 13 | maxWidth: 640, 14 | height: 360, 15 | margin: '0 auto', 16 | }) 17 | 18 | const interval = number('Interval', 5000, { 19 | range: true, 20 | min: 0, 21 | max: 100000, 22 | step: 1000, 23 | }) 24 | 25 | const carousel = new Carousel(container, { interval }) 26 | 27 | const commonStyle = 28 | 'position:relative;height:100%;width:100%;background-size:contain;background-repeat:no-repeat;background-position:center;' 29 | 30 | carousel.append( 31 | `
` 32 | ) 33 | carousel.append( 34 | `
` 35 | ) 36 | carousel.append( 37 | `
` 38 | ) 39 | carousel.append( 40 | `
` 41 | ) 42 | 43 | button('Clear', () => { 44 | carousel.clear() 45 | return false 46 | }) 47 | 48 | return carousel 49 | }, 50 | { 51 | readme, 52 | source: __STORY__, 53 | } 54 | ) 55 | 56 | export default def 57 | 58 | export const { carousel } = def 59 | -------------------------------------------------------------------------------- /src/carousel/test.js: -------------------------------------------------------------------------------- 1 | import Carousel from './index' 2 | import test from '../share/test' 3 | 4 | test('carousel', (container) => { 5 | const carousel = new Carousel(container) 6 | 7 | it('basic', function () { 8 | carousel.append('Item 1') 9 | const $item = $(container).find(carousel.c('.item')) 10 | expect($item.html()).to.equal('Item 1') 11 | }) 12 | 13 | return carousel 14 | }) 15 | -------------------------------------------------------------------------------- /src/chart/BaseChart.ts: -------------------------------------------------------------------------------- 1 | import Chart from './index' 2 | 3 | export default abstract class BaseChart { 4 | protected chart: Chart 5 | constructor(chart: Chart) { 6 | this.chart = chart 7 | } 8 | abstract draw(): void 9 | } 10 | -------------------------------------------------------------------------------- /src/chart/README.md: -------------------------------------------------------------------------------- 1 | # Luna Chart 2 | 3 | HTML5 charts. 4 | 5 | ## Demo 6 | 7 | https://luna.liriliri.io/?path=/story/chart 8 | 9 | ## Install 10 | 11 | Add the following script and style to your page. 12 | 13 | ```html 14 | 15 | ``` 16 | 17 | You can also get it on npm. 18 | 19 | ```bash 20 | npm install luna-chart --save 21 | ``` 22 | 23 | ```javascript 24 | import LunaChart from 'luna-chart' 25 | ``` 26 | 27 | ## Usage 28 | 29 | ```javascript 30 | const container = document.getElementById('container') 31 | const barChart = new LunaChart(container, { 32 | type: 'bar', 33 | bgColor: '#fbfbfb', 34 | title: { 35 | text: 'Bar Chart', 36 | }, 37 | data: { 38 | labels: ['Monday', 'TuesDay', 'Wednesday', 'Thursday', 'Friday'], 39 | datasets: [ 40 | { 41 | label: 'Dataset 1', 42 | bgColor: '#e73c5e', 43 | data: [128, 146, 56, 84, 222], 44 | }, 45 | { 46 | label: '#614d82', 47 | bgColor: '#614d82', 48 | data: [119, 23, 98, 67, 88], 49 | }, 50 | ], 51 | }, 52 | }) 53 | ``` 54 | -------------------------------------------------------------------------------- /src/chart/RingChart.ts: -------------------------------------------------------------------------------- 1 | import PieChart from './PieChart' 2 | import { px } from './util' 3 | 4 | export default class RingChart extends PieChart { 5 | draw() { 6 | super.draw() 7 | 8 | const { chart } = this 9 | const { ctx, canvas } = chart 10 | const bgColor = chart.getOption('bgColor') 11 | 12 | const x = canvas.width / 2 13 | const y = canvas.height / 2 14 | 15 | ctx.beginPath() 16 | ctx.fillStyle = bgColor 17 | ctx.arc(x, y, px(60), 0, 2 * Math.PI) 18 | ctx.closePath() 19 | ctx.fill() 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/chart/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chart", 3 | "version": "0.1.0", 4 | "description": "HTML5 charts", 5 | "luna": { 6 | "style": false, 7 | "install": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/chart/test.js: -------------------------------------------------------------------------------- 1 | import Chart from './index' 2 | import test from '../share/test' 3 | 4 | test('chart', (container) => { 5 | $(container).css({ 6 | width: 600, 7 | height: 300, 8 | }) 9 | 10 | it('bar', function () { 11 | const chart = new Chart(container, { 12 | type: 'bar', 13 | bgColor: '#fbfbfb', 14 | title: { 15 | text: 'Bar Chart', 16 | }, 17 | data: { 18 | labels: ['Monday', 'TuesDay', 'Wednesday', 'Thursday', 'Friday'], 19 | datasets: [ 20 | { 21 | label: 'Dataset 1', 22 | bgColor: '#e73c5e', 23 | data: [128, 146, 56, 84, 222], 24 | }, 25 | { 26 | label: '#614d82', 27 | bgColor: '#614d82', 28 | data: [119, 23, 98, 67, 88], 29 | }, 30 | ], 31 | }, 32 | }) 33 | 34 | expect(chart).to.be.an('object') 35 | }) 36 | }) 37 | -------------------------------------------------------------------------------- /src/chart/util.ts: -------------------------------------------------------------------------------- 1 | const DPI = window.devicePixelRatio || 1 2 | 3 | export function px(val: number) { 4 | return val * DPI 5 | } 6 | -------------------------------------------------------------------------------- /src/color-picker/README.md: -------------------------------------------------------------------------------- 1 | # Luna Color Picker 2 | 3 | Color picker. 4 | 5 | ## Demo 6 | 7 | https://luna.liriliri.io/?path=/story/color-picker 8 | 9 | ## Install 10 | 11 | Add the following script and style to your page. 12 | 13 | ```html 14 | 15 | 16 | ``` 17 | 18 | You can also get it on npm. 19 | 20 | ```bash 21 | npm install luna-color-picker --save 22 | ``` 23 | 24 | ```javascript 25 | import 'luna-color-picker/luna-color-picker.css' 26 | import LunaColorPicker from 'luna-color-picker' 27 | ``` 28 | 29 | ## Usage 30 | 31 | ```javascript 32 | const colorPicker = new LunaColorPicker(container) 33 | ``` 34 | -------------------------------------------------------------------------------- /src/color-picker/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "color-picker", 3 | "version": "0.1.0", 4 | "description": "Color picker", 5 | "luna": { 6 | "test": false 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/color-picker/story.js: -------------------------------------------------------------------------------- 1 | import 'luna-color-picker.css' 2 | import ColorPicker from 'luna-color-picker' 3 | import story from '../share/story' 4 | import readme from './README.md' 5 | 6 | const def = story( 7 | 'color-picker', 8 | (container) => { 9 | const colorPicker = new ColorPicker(container) 10 | 11 | return colorPicker 12 | }, 13 | { 14 | readme, 15 | source: __STORY__, 16 | } 17 | ) 18 | 19 | export default def 20 | 21 | export const { colorPicker } = def 22 | -------------------------------------------------------------------------------- /src/color-picker/style.scss: -------------------------------------------------------------------------------- 1 | @use '../share/mixin' as mixin; 2 | @use '../share/theme' as theme; 3 | 4 | .luna-color-picker { 5 | border: 1px solid theme.$color-border; 6 | touch-action: none; 7 | width: 225px; 8 | @include mixin.component(); 9 | } 10 | 11 | .saturation { 12 | position: relative; 13 | padding-bottom: 55%; 14 | overflow: hidden; 15 | } 16 | 17 | .saturation-white, 18 | .saturation-black { 19 | position: absolute; 20 | inset: 0; 21 | } 22 | 23 | .saturation-white { 24 | background: linear-gradient(to right, #fff, rgba(255, 255, 255, 0)); 25 | } 26 | 27 | .saturation-black { 28 | background: linear-gradient(to top, #000, rgba(0, 0, 0, 0)); 29 | } 30 | 31 | .saturation-pointer { 32 | position: absolute; 33 | width: 12px; 34 | height: 12px; 35 | border-radius: 6px; 36 | transform: translate(-6px, -6px); 37 | box-shadow: rgb(255, 255, 255) 0px 0px 0px 1px inset; 38 | } 39 | 40 | .body { 41 | padding: 16px 16px 12px; 42 | } 43 | 44 | .controls { 45 | display: flex; 46 | } 47 | 48 | .color { 49 | width: 32px; 50 | } 51 | 52 | .swatch-container { 53 | margin-top: 6px; 54 | width: 16px; 55 | height: 16px; 56 | border-radius: 8px; 57 | position: relative; 58 | overflow: hidden; 59 | } 60 | 61 | .swatch { 62 | width: 100%; 63 | height: 100%; 64 | box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.1); 65 | } 66 | -------------------------------------------------------------------------------- /src/color-picker/util.ts: -------------------------------------------------------------------------------- 1 | const round = Math.round 2 | 3 | export function rgbToHsv(r: number, g: number, b: number) { 4 | let h = 0 5 | const max = Math.max(r, g, b) 6 | const min = Math.min(r, g, b) 7 | const delta = max - min 8 | 9 | if (delta === 0) { 10 | h = 0 11 | } else if (r === max) { 12 | h = ((g - b) / delta) % 6 13 | } else if (g === max) { 14 | h = (b - r) / delta + 2 15 | } else if (b === max) { 16 | h = (r - g) / delta + 4 17 | } 18 | 19 | h = round(h * 60) 20 | if (h < 0) h += 360 21 | 22 | const s = round((max === 0 ? 0 : delta / max) * 100) 23 | 24 | const v = round((max / 255) * 100) 25 | 26 | return [h, s, v] 27 | } 28 | 29 | export function hsvToRgb(h: number, s: number, v: number) { 30 | s = s / 100 31 | v = v / 100 32 | const c = v * s 33 | const p = h / 60 34 | const x = c * (1 - Math.abs((p % 2) - 1)) 35 | const m = v - c 36 | 37 | const rgb = 38 | p === 0 39 | ? [c, x, 0] 40 | : p === 1 41 | ? [x, c, 0] 42 | : p === 2 43 | ? [0, c, x] 44 | : p === 3 45 | ? [0, x, c] 46 | : p === 4 47 | ? [x, 0, c] 48 | : p === 5 49 | ? [c, 0, x] 50 | : [] 51 | 52 | return [ 53 | round(255 * (rgb[0] + m)), 54 | round(255 * (rgb[1] + m)), 55 | round(255 * (rgb[2] + m)), 56 | ] 57 | } 58 | -------------------------------------------------------------------------------- /src/command-palette/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "command-palette", 3 | "version": "0.3.1", 4 | "description": "Command palette", 5 | "luna": { 6 | "react": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/command-palette/test.js: -------------------------------------------------------------------------------- 1 | import trim from 'licia/trim' 2 | import CommandPalette from './index' 3 | import test from '../share/test' 4 | 5 | test('command-palette', (container) => { 6 | const commandPalette = new CommandPalette(container, { 7 | commands: [ 8 | { 9 | title: 'Do Nothing', 10 | handler() {}, 11 | }, 12 | ], 13 | }) 14 | commandPalette.show() 15 | 16 | it('basic', function () { 17 | const $command = $(container).find(commandPalette.c('.list') + ' li') 18 | expect(trim($command.text())).to.equal('Do Nothing') 19 | }) 20 | 21 | return commandPalette 22 | }) 23 | -------------------------------------------------------------------------------- /src/console/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.3.6 (15 Apr 2025) 2 | 3 | * fix: errors cannot be copied [#26](https://github.com/liriliri/luna/issues/26) 4 | * fix: luna-console added to variable names [#25](https://github.com/liriliri/luna/issues/25) 5 | 6 | ## 1.3.5 (10 Oct 2024) 7 | 8 | * fix: DOMException not treated as error [#18](https://github.com/liriliri/luna/issues/18) 9 | 10 | ## 1.3.4 (8 July 2024) 11 | 12 | * fix: scroll bottom check [#17](https://github.com/liriliri/luna/pull/17) 13 | 14 | ## 1.3.3 (12 June 2023) 15 | 16 | * fix: print string with %o 17 | 18 | ## 1.3.2 (19 Apr 2023) 19 | 20 | * fix: position fixed not working [#9](https://github.com/liriliri/luna/issues/9) 21 | 22 | ## 1.3.1 (6 Feb 2023) 23 | 24 | * fix: filter ingore case 25 | * fix: keep info type 26 | 27 | ## 1.3.0 (2 Jan 2023) 28 | 29 | * feat: support log level 30 | * fix: element dir name 31 | 32 | ## 1.2.0 (12 Dec 2022) 33 | 34 | * feat: support bigint 35 | * fix: primitives highlight 36 | 37 | ## 1.1.3 (9 Dec 2022) 38 | 39 | * feat: support esm 40 | 41 | ## 1.1.2 (7 Dec 2022) 42 | 43 | * fix: remove debug log 44 | 45 | ## 1.1.1 (5 Dec 2022) 46 | 47 | * fix: pc scroll performance 48 | 49 | ## 1.1.0 (3 Dec 2022) 50 | 51 | * feat: truncate long string 52 | * feat: object preview toggle icon 53 | * fix: ie11 empty 54 | 55 | ## 1.0.0 (19 Nov 2022) 56 | 57 | * feat: dark mode 58 | * feat: use data-grid to display table 59 | * feat: use dom-viewer to display element 60 | * feat: select and copy -------------------------------------------------------------------------------- /src/console/icon.json: -------------------------------------------------------------------------------- 1 | [ 2 | "caret-down.svg", 3 | "caret-right.svg", 4 | "warn.svg", 5 | "error.svg", 6 | "input.svg", 7 | "output.svg" 8 | ] 9 | -------------------------------------------------------------------------------- /src/console/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "console", 3 | "version": "1.3.6", 4 | "description": "Console for logging", 5 | "luna": { 6 | "icon": true, 7 | "dependencies": [ 8 | "object-viewer", 9 | "data-grid", 10 | "dom-viewer" 11 | ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/console/util.ts: -------------------------------------------------------------------------------- 1 | import upperFirst from 'licia/upperFirst' 2 | 3 | export function getObjType(obj: any) { 4 | if (obj.constructor && obj.constructor.name) return obj.constructor.name 5 | 6 | return upperFirst({}.toString.call(obj).replace(/(\[object )|]/g, '')) 7 | } 8 | -------------------------------------------------------------------------------- /src/cropper/README.md: -------------------------------------------------------------------------------- 1 | # Luna Cropper 2 | 3 | Image cropper. 4 | 5 | ## Demo 6 | 7 | https://luna.liriliri.io/?path=/story/cropper 8 | 9 | ## Install 10 | 11 | Add the following script and style to your page. 12 | 13 | ```html 14 | 15 | 16 | ``` 17 | 18 | You can also get it on npm. 19 | 20 | ```bash 21 | npm install luna-cropper --save 22 | ``` 23 | 24 | ```javascript 25 | import 'luna-cropper/luna-cropper.css' 26 | import LunaCropper from 'luna-cropper' 27 | ``` 28 | 29 | ## Usage 30 | 31 | ```javascript 32 | const container = document.getElementById('container') 33 | const cropper = new LunaCropper(container, { 34 | image: 'https://luna.liriliri.io/wallpaper.png', 35 | }) 36 | console.log(cropper.getData()) 37 | ``` 38 | 39 | ## Configuration 40 | 41 | * image(string): Image url. 42 | * preview(HTMLElement): Preview dom container. 43 | 44 | ## Api 45 | 46 | ### getCanvas(): HTMLCanvasElement 47 | 48 | Get a canvas with cropped image drawn. 49 | 50 | ### getData(): object 51 | 52 | Get size, position data of image and crop box. 53 | 54 | ### reset(): void 55 | 56 | Resize crop box. 57 | -------------------------------------------------------------------------------- /src/cropper/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cropper", 3 | "version": "0.2.1", 4 | "description": "Image cropper", 5 | "luna": { 6 | "react": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/cropper/react.tsx: -------------------------------------------------------------------------------- 1 | import { CSSProperties, FC, useEffect, useRef } from 'react' 2 | import Cropper, { IOptions } from './index' 3 | import each from 'licia/each' 4 | import { useNonInitialEffect } from '../share/hooks' 5 | import clone from 'licia/clone' 6 | 7 | interface ICropperProps extends IOptions { 8 | style?: CSSProperties 9 | className?: string 10 | onCreate?: (cropper: Cropper) => void 11 | } 12 | 13 | const LunaCropper: FC = (props) => { 14 | const cropperRef = useRef(null) 15 | const cropper = useRef() 16 | 17 | useEffect(() => { 18 | cropper.current = new Cropper(cropperRef.current!, clone(props)) 19 | props.onCreate && props.onCreate(cropper.current) 20 | 21 | return () => cropper.current?.destroy() 22 | }, []) 23 | 24 | each(['image', 'preview'], (key: keyof ICropperProps) => { 25 | useNonInitialEffect(() => { 26 | if (cropper.current) { 27 | cropper.current.setOption(key, props[key]) 28 | } 29 | }, [props[key]]) 30 | }) 31 | 32 | return ( 33 |
38 | ) 39 | } 40 | 41 | export default LunaCropper 42 | -------------------------------------------------------------------------------- /src/cropper/test.js: -------------------------------------------------------------------------------- 1 | import Cropper from './index' 2 | import test from '../share/test' 3 | 4 | test('cropper', (container) => { 5 | const cropper = new Cropper(container, { 6 | url: 'https://luna.liriliri.io/wallpaper.png', 7 | }) 8 | 9 | it('basic', function () { 10 | expect(cropper.getData()).to.be.a('object') 11 | }) 12 | 13 | return cropper 14 | }) 15 | -------------------------------------------------------------------------------- /src/danmaku/README.md: -------------------------------------------------------------------------------- 1 | # Luna Danmaku 2 | 3 | Live comment player. 4 | 5 | ## Demo 6 | 7 | https://luna.liriliri.io/?path=/story/danmaku 8 | 9 | ## Install 10 | 11 | Add the following script and style to your page. 12 | 13 | ```html 14 | 15 | 16 | ``` 17 | 18 | You can also get it on npm. 19 | 20 | ```bash 21 | npm install luna-danmaku --save 22 | ``` 23 | 24 | ```javascript 25 | import 'luna-danmaku/luna-danmaku.css' 26 | import LunaDanmaku from 'luna-danmaku' 27 | ``` 28 | -------------------------------------------------------------------------------- /src/danmaku/index.ts: -------------------------------------------------------------------------------- 1 | import Component from '../share/Component' 2 | 3 | /** 4 | * Live comment player. 5 | */ 6 | export default class Danmaku extends Component { 7 | constructor(container: HTMLElement) { 8 | super(container, { compName: 'danmaku' }) 9 | } 10 | } 11 | 12 | module.exports = Danmaku 13 | module.exports.default = Danmaku 14 | -------------------------------------------------------------------------------- /src/danmaku/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "danmaku", 3 | "version": "0.1.0", 4 | "description": "Live comment player", 5 | "luna": { 6 | "test": false 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/danmaku/story.js: -------------------------------------------------------------------------------- 1 | import 'luna-danmaku.css' 2 | import story from '../share/story' 3 | import Danmaku from 'luna-danmaku.js' 4 | import readme from './README.md' 5 | 6 | const def = story( 7 | 'danmaku', 8 | (container) => { 9 | const danmaku = new Danmaku(container) 10 | 11 | return danmaku 12 | }, 13 | { 14 | readme, 15 | source: __STORY__, 16 | } 17 | ) 18 | 19 | export default def 20 | 21 | export const { danmaku } = def 22 | -------------------------------------------------------------------------------- /src/danmaku/style.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liriliri/luna/09cfb448f0e8646fb790feb6ca03e6799e5dc697/src/danmaku/style.scss -------------------------------------------------------------------------------- /src/data-grid/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.4.3 (30 May 2025) 2 | 3 | * fix: update height multiple times when set data 4 | 5 | ## 1.4.2 (23 Apr 2025) 6 | 7 | * fix: sort nodes performance 8 | 9 | ## 1.4.1 (23 Mar 2025) 10 | 11 | * fix: Incorrect display when setting filter 12 | 13 | ## 1.4.0 (17 Mar 2025) 14 | 15 | * feat: virtual list 16 | 17 | ## 1.3.2 (2 Mar 2025) 18 | 19 | * refactor: theme 20 | 21 | ## 1.3.1 (26 Feb 2025) 22 | 23 | * refactor: theme 24 | 25 | ## 1.3.0 (12 Jan 2025) 26 | 27 | * feat: contextmenu event 28 | * feat: click and dblclick event 29 | 30 | ## 1.2.1 (22 Nov 2024) 31 | 32 | * fix: scroll position not restored 33 | 34 | ## 1.2.0 (15 Nov 2024) 35 | 36 | * feat: setData api 37 | * perf: append a lot 38 | 39 | ## 1.1.0 (3 Nov 2024) 40 | 41 | * feat: add auto theme 42 | 43 | ## 1.0.0 (12 Sep 2024) 44 | 45 | * feat: sortable icons 46 | -------------------------------------------------------------------------------- /src/data-grid/icon.json: -------------------------------------------------------------------------------- 1 | ["caret-down.svg", "caret-up.svg"] 2 | -------------------------------------------------------------------------------- /src/data-grid/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "data-grid", 3 | "version": "1.4.3", 4 | "description": "Grid for displaying datasets", 5 | "luna": { 6 | "react": true, 7 | "icon": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/data-grid/test.js: -------------------------------------------------------------------------------- 1 | import DataGrid from './index' 2 | import test from '../share/test' 3 | 4 | test('data-grid', (container) => { 5 | it('basic', function () { 6 | const dataGrid = new DataGrid(container, { 7 | columns: [ 8 | { 9 | id: 'index', 10 | title: 'Index', 11 | weight: 20, 12 | sortable: true, 13 | }, 14 | { 15 | id: 'name', 16 | title: 'Name', 17 | sortable: true, 18 | weight: 30, 19 | }, 20 | { 21 | id: 'site', 22 | title: 'Site', 23 | }, 24 | ], 25 | }) 26 | dataGrid.append({ 27 | index: 0, 28 | name: 'Taobao', 29 | site: 'www.taobao.com', 30 | }) 31 | }) 32 | }) 33 | -------------------------------------------------------------------------------- /src/dom-highlighter/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.0.2 (27 Nov 2022) 2 | 3 | * fix: ie11 string endsWith not supported 4 | 5 | ## 1.0.1 (26 Nov 2022) 6 | 7 | * fix: support ie11 8 | 9 | ## 1.0.0 (10 Nov 2022) 10 | 11 | * fix: remove canvas id liriliri/eruda#269 -------------------------------------------------------------------------------- /src/dom-highlighter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dom-highlighter", 3 | "version": "1.0.2", 4 | "description": "Highlighter for html elements", 5 | "dependencies": { 6 | "path2d-polyfill": "^1.2.3" 7 | }, 8 | "luna": { 9 | "install": true 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/dom-highlighter/test.js: -------------------------------------------------------------------------------- 1 | import DomHighlighter from './index' 2 | import test from '../share/test' 3 | 4 | test('dom-highlighter', (container) => { 5 | const domHighlighter = new DomHighlighter(container, { 6 | showRulers: true, 7 | }) 8 | 9 | it('basic', function () { 10 | domHighlighter.highlight(document.body) 11 | domHighlighter.hide() 12 | }) 13 | 14 | return domHighlighter 15 | }) 16 | -------------------------------------------------------------------------------- /src/dom-viewer/README.md: -------------------------------------------------------------------------------- 1 | # Luna Dom Viewer 2 | 3 | Dom tree navigator. 4 | 5 | ## Demo 6 | 7 | https://luna.liriliri.io/?path=/story/dom-viewer 8 | 9 | ## Install 10 | 11 | Add the following script and style to your page. 12 | 13 | ```html 14 | 15 | 16 | ``` 17 | 18 | You can also get it on npm. 19 | 20 | ```bash 21 | npm install luna-dom-viewer --save 22 | ``` 23 | 24 | ```javascript 25 | import 'luna-dom-viewer/luna-dom-viewer.css' 26 | import LunaDomViewer from 'luna-dom-viewer' 27 | ``` 28 | 29 | ## Usage 30 | 31 | ```javascript 32 | const container = document.getElementById('container') 33 | const domViewer = new LunaDomViewer(container) 34 | domViewer.expand() 35 | ``` 36 | 37 | ## Configuration 38 | 39 | * hotkey(boolean): Enable hotkey. 40 | * ignore(AnyFn): Predicate function which removes the matching child nodes. 41 | * ignoreAttr(AnyFn): Predicate function which removes the matching node attributes. 42 | * lowerCaseTagName(boolean): Whether to convert tag name to lower case. 43 | * node(ChildNode): Html element to navigate. 44 | * observe(boolean): Observe dom mutation. 45 | 46 | ## Api 47 | 48 | ### select(node?: ChildNode): void 49 | 50 | Select given node. 51 | -------------------------------------------------------------------------------- /src/dom-viewer/icon.json: -------------------------------------------------------------------------------- 1 | ["caret-down.svg", "caret-right.svg"] 2 | -------------------------------------------------------------------------------- /src/dom-viewer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dom-viewer", 3 | "version": "1.8.3", 4 | "description": "Dom tree navigator", 5 | "luna": { 6 | "icon": true, 7 | "react": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/dom-viewer/react.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useEffect, useRef } from 'react' 2 | import each from 'licia/each' 3 | import DomViewer, { IOptions } from './index' 4 | import { useEvent, useOption, usePrevious } from '../share/hooks' 5 | 6 | interface IDomViewerProps extends IOptions { 7 | onCreate?: (domViewer: DomViewer) => void 8 | onSelect?: (node: Node) => void 9 | onDeselect?: () => void 10 | } 11 | 12 | const LunaDomViewer: FC = (props) => { 13 | const domViewerRef = useRef(null) 14 | const domViewer = useRef() 15 | const prevProps = usePrevious(props) 16 | 17 | useEffect(() => { 18 | domViewer.current = new DomViewer(domViewerRef.current!, { 19 | theme: props.theme, 20 | hotkey: props.hotkey, 21 | node: props.node, 22 | ignore: props.ignore, 23 | ignoreAttr: props.ignoreAttr, 24 | observe: props.observe, 25 | lowerCaseTagName: props.lowerCaseTagName, 26 | }) 27 | props.onCreate && props.onCreate(domViewer.current) 28 | 29 | return () => domViewer.current?.destroy() 30 | }, []) 31 | 32 | useEvent(domViewer, 'select', prevProps?.onSelect, props.onSelect) 33 | useEvent( 34 | domViewer, 35 | 'deselect', 36 | prevProps?.onDeselect, 37 | props.onDeselect 38 | ) 39 | 40 | each(['theme'], (key: keyof IDomViewerProps) => { 41 | useOption(domViewer, key, props[key]) 42 | }) 43 | 44 | return
45 | } 46 | 47 | export default LunaDomViewer 48 | -------------------------------------------------------------------------------- /src/dom-viewer/test.js: -------------------------------------------------------------------------------- 1 | import DomViewer from './index' 2 | import test from '../share/test' 3 | 4 | test('dom-viewer', (container) => { 5 | const domViewer = new DomViewer(container) 6 | 7 | it('basic', function () { 8 | domViewer.expand() 9 | domViewer.collapse() 10 | }) 11 | 12 | return domViewer 13 | }) 14 | -------------------------------------------------------------------------------- /src/drag-selector/README.md: -------------------------------------------------------------------------------- 1 | # Luna Drag Selector 2 | 3 | Drag selector for selecting multiple items. 4 | 5 | ## Demo 6 | 7 | https://luna.liriliri.io/?path=/story/drag-selector 8 | 9 | ## Install 10 | 11 | Add the following script and style to your page. 12 | 13 | ```html 14 | 15 | 16 | ``` 17 | 18 | You can also get it on npm. 19 | 20 | ```bash 21 | npm install luna-drag-selector --save 22 | ``` 23 | 24 | ```javascript 25 | import 'luna-drag-selector/luna-drag-selector.css' 26 | import LunaDragSelector from 'luna-drag-selector' 27 | ``` 28 | 29 | ## Usage 30 | 31 | ```javascript 32 | const dragSelector = new DragSelector(container) 33 | let selectedElements = [] 34 | dragSelector.on('select', () => { 35 | selectedElements = [] 36 | if (dragSelector.isSelected(itemElement)) { 37 | selectedElements.push(itemElement) 38 | } 39 | }) 40 | dragSelector.on('change', () => { 41 | console.log('Selection changed:', selectedElements) 42 | }) 43 | ``` 44 | 45 | ## Api 46 | 47 | ### isSelected(el: HTMLElement): boolean 48 | 49 | Check whether an element is selected. 50 | -------------------------------------------------------------------------------- /src/drag-selector/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "drag-selector", 3 | "version": "0.1.0", 4 | "description": "Drag selector for selecting multiple items" 5 | } 6 | -------------------------------------------------------------------------------- /src/drag-selector/style.scss: -------------------------------------------------------------------------------- 1 | @use '../share/mixin' as *; 2 | @use '../share/theme' as *; 3 | @use 'sass:color'; 4 | 5 | .luna-drag-selector { 6 | position: relative; 7 | user-select: none; 8 | } 9 | 10 | .select-area { 11 | position: absolute; 12 | border: 1px solid; 13 | z-index: 1000; 14 | } 15 | 16 | @each $theme in ('light', 'dark') { 17 | .theme-#{$theme} { 18 | .select-area { 19 | @include theme-var(border-color, color-primary, $theme); 20 | background-color: if( 21 | $theme == 'light', 22 | color.change($color-primary, $alpha: 0.2), 23 | color.change($color-primary-dark, $alpha: 0.2) 24 | ); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/drag-selector/test.js: -------------------------------------------------------------------------------- 1 | import DragSelector from './index' 2 | import test from '../share/test' 3 | import h from 'licia/h' 4 | 5 | test('drag-selector', (container) => { 6 | const item = h('div') 7 | container.appendChild(item) 8 | const dragSelector = new DragSelector(container) 9 | 10 | it('basic', function () { 11 | expect(dragSelector.isSelected(item)).to.be.false 12 | }) 13 | 14 | return dragSelector 15 | }) 16 | -------------------------------------------------------------------------------- /src/editor/README.md: -------------------------------------------------------------------------------- 1 | # Luna Editor 2 | 3 | Wysiwyg editor. 4 | 5 | ## Demo 6 | 7 | https://luna.liriliri.io/?path=/story/editor 8 | 9 | ## Install 10 | 11 | Add the following script and style to your page. 12 | 13 | ```html 14 | 15 | 16 | ``` 17 | 18 | You can also get it on npm. 19 | 20 | ```bash 21 | npm install luna-editor --save 22 | ``` 23 | 24 | ```javascript 25 | import 'luna-editor/luna-editor.css' 26 | import LunaEditor from 'luna-editor' 27 | ``` 28 | 29 | ## Usage 30 | 31 | ```javascript 32 | const container = document.getElementById('container') 33 | const editor = new LunaEditor(container) 34 | console.log(editor.html()) 35 | ``` 36 | -------------------------------------------------------------------------------- /src/editor/Selection.ts: -------------------------------------------------------------------------------- 1 | export default class Selection {} 2 | -------------------------------------------------------------------------------- /src/editor/icon.json: -------------------------------------------------------------------------------- 1 | [ 2 | "fullscreen.svg", 3 | "header.svg", 4 | "bold.svg", 5 | "italic.svg", 6 | "horizontal-rule.svg", 7 | "quote.svg", 8 | "strike-through.svg", 9 | "underline.svg" 10 | ] 11 | -------------------------------------------------------------------------------- /src/editor/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "editor", 3 | "version": "0.1.0", 4 | "description": "Wysiwyg editor", 5 | "dependencies": { 6 | "markdown-it": "^12.3.2" 7 | }, 8 | "luna": { 9 | "icon": true, 10 | "install": true 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/editor/story.js: -------------------------------------------------------------------------------- 1 | import 'luna-editor.css' 2 | import Editor from 'luna-editor.js' 3 | import h from 'licia/h' 4 | import $ from 'licia/$' 5 | import story from '../share/story' 6 | import readme from './README.md' 7 | import { text } from '@storybook/addon-knobs' 8 | import MarkdownIt from 'markdown-it' 9 | 10 | const md = new MarkdownIt({ linkify: true }) 11 | 12 | const def = story( 13 | 'editor', 14 | (wrapper) => { 15 | $(wrapper).html('') 16 | 17 | const content = text('Initial Content', md.render(readme)) 18 | 19 | const toolbarContainer = h('div') 20 | $(toolbarContainer).css({ 21 | border: '1px solid #eee', 22 | }) 23 | const toolbar = new Editor.Toolbar(toolbarContainer) 24 | wrapper.appendChild(toolbarContainer) 25 | 26 | const editorContainerA = h('div') 27 | $(editorContainerA).css('marginTop', 10) 28 | editorContainerA.innerHTML = content 29 | const editorContainerB = h('div') 30 | $(editorContainerB).css('marginTop', 10) 31 | editorContainerB.innerHTML = editorContainerA.innerHTML 32 | wrapper.appendChild(editorContainerA) 33 | wrapper.appendChild(editorContainerB) 34 | 35 | const editorA = new Editor(editorContainerA, { 36 | toolbar, 37 | }) 38 | 39 | const editorB = new Editor(editorContainerB) 40 | 41 | return [editorA, editorB] 42 | }, 43 | { 44 | readme, 45 | source: __STORY__, 46 | } 47 | ) 48 | 49 | export default def 50 | 51 | export const { editor } = def 52 | -------------------------------------------------------------------------------- /src/editor/test.js: -------------------------------------------------------------------------------- 1 | import Editor from './index' 2 | import test from '../share/test' 3 | 4 | test('editor', (container) => { 5 | container.innerHTML = 'luna' 6 | const editor = new Editor(container) 7 | 8 | it('basic', function () { 9 | expect(editor.html()).to.equal('luna') 10 | }) 11 | 12 | return editor 13 | }) 14 | -------------------------------------------------------------------------------- /src/file-list/asset/audio.svg: -------------------------------------------------------------------------------- 1 | 3 | 6 | 8 | 11 | -------------------------------------------------------------------------------- /src/file-list/asset/file.svg: -------------------------------------------------------------------------------- 1 | 3 | 6 | 8 | -------------------------------------------------------------------------------- /src/file-list/asset/folder.svg: -------------------------------------------------------------------------------- 1 | 3 | 6 | 8 | -------------------------------------------------------------------------------- /src/file-list/asset/image.svg: -------------------------------------------------------------------------------- 1 | 3 | 6 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /src/file-list/asset/text.svg: -------------------------------------------------------------------------------- 1 | 3 | 6 | 8 | 11 | -------------------------------------------------------------------------------- /src/file-list/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "file-list", 3 | "version": "0.4.0", 4 | "description": "List files in the directory", 5 | "luna": { 6 | "react": true, 7 | "install": true, 8 | "dependencies": [ 9 | "data-grid", 10 | "icon-list" 11 | ] 12 | }, 13 | "dependencies": { 14 | "stat-mode": "^1.0.0" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/file-list/style.scss: -------------------------------------------------------------------------------- 1 | @use '../share/mixin' as mixin; 2 | 3 | .luna-file-list { 4 | @include mixin.component(true); 5 | .luna-icon-list { 6 | height: 100%; 7 | } 8 | .luna-data-grid { 9 | img { 10 | height: 14px; 11 | width: 14px; 12 | margin: 0; 13 | padding: 0; 14 | margin-right: 2px; 15 | flex-shrink: 0; 16 | object-fit: contain; 17 | vertical-align: top; 18 | } 19 | } 20 | } 21 | 22 | .hidden { 23 | display: none !important; 24 | } 25 | -------------------------------------------------------------------------------- /src/file-list/test.js: -------------------------------------------------------------------------------- 1 | import FileList from './index' 2 | import test from '../share/test' 3 | 4 | test('file-list', (container) => { 5 | it('basic', function () { 6 | const fileList = new FileList(container, { 7 | files: [ 8 | { 9 | name: 'test.txt', 10 | size: 1024, 11 | directory: false, 12 | mtime: new Date(), 13 | }, 14 | { 15 | name: 'folder 1', 16 | directory: true, 17 | mtime: new Date(), 18 | }, 19 | { 20 | name: 'picture.jpg', 21 | thumbnail: '', 22 | size: 2048, 23 | directory: false, 24 | }, 25 | ], 26 | }) 27 | }) 28 | }) 29 | -------------------------------------------------------------------------------- /src/gallery/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.0.2 (23 Jan 2025) 2 | 3 | * chore: update dependencies 4 | 5 | ## 1.0.1 (7 Sep 2024) 6 | 7 | * fix: esm import 8 | 9 | ## 1.0.0 (23 Aug 2024) 10 | 11 | * feat: zoom in and zoom out 12 | -------------------------------------------------------------------------------- /src/gallery/icon.json: -------------------------------------------------------------------------------- 1 | [ 2 | "fullscreen.svg", 3 | "pause.svg", 4 | "play.svg", 5 | "close.svg", 6 | "zoom-in.svg", 7 | "zoom-out.svg", 8 | "download.svg", 9 | "original.svg" 10 | ] 11 | -------------------------------------------------------------------------------- /src/gallery/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gallery", 3 | "version": "1.0.2", 4 | "description": "Lightweight gallery", 5 | "luna": { 6 | "icon": true, 7 | "dependencies": [ 8 | "carousel", 9 | "image-viewer", 10 | "toolbar" 11 | ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/gallery/story.js: -------------------------------------------------------------------------------- 1 | import 'luna-gallery.css' 2 | import Gallery from 'luna-gallery.js' 3 | import $ from 'licia/$' 4 | import readme from './README.md' 5 | import changelog from './CHANGELOG.md' 6 | import story from '../share/story' 7 | import { button, boolean } from '@storybook/addon-knobs' 8 | 9 | const def = story( 10 | 'gallery', 11 | (container) => { 12 | $(container).css({ 13 | width: '100%', 14 | maxWidth: 640, 15 | height: 360, 16 | margin: '0 auto', 17 | }) 18 | 19 | const inline = boolean('Inline Mode', false) 20 | 21 | const gallery = new Gallery(container, { 22 | inline, 23 | }) 24 | gallery.show() 25 | 26 | gallery.append('/pic1.png', 'pic1.png') 27 | gallery.append('/pic2.png', 'pic2.png') 28 | gallery.append('/pic3.png', 'pic3.png') 29 | gallery.append('/pic4.png', 'pic4.png') 30 | 31 | button('Show', () => { 32 | gallery.show() 33 | return false 34 | }) 35 | 36 | button('Clear', () => { 37 | gallery.clear() 38 | return false 39 | }) 40 | 41 | return gallery 42 | }, 43 | { 44 | readme, 45 | changelog, 46 | source: __STORY__, 47 | } 48 | ) 49 | 50 | export default def 51 | 52 | export const { gallery } = def 53 | -------------------------------------------------------------------------------- /src/gallery/style.scss: -------------------------------------------------------------------------------- 1 | @use '../share/mixin' as mixin; 2 | @use '../share/theme' as theme; 3 | 4 | .luna-gallery { 5 | position: relative; 6 | @include mixin.component(); 7 | & { 8 | background: black; 9 | } 10 | &.full { 11 | position: fixed; 12 | background: rgba(0, 0, 0, 50%); 13 | left: 0; 14 | top: 0; 15 | z-index: 10; 16 | width: 100% !important; 17 | height: 100% !important; 18 | max-width: 100% !important; 19 | } 20 | .luna-carousel { 21 | background: transparent; 22 | } 23 | .luna-toolbar { 24 | background: rgba(0, 0, 0, 0.45) !important; 25 | border-bottom: none; 26 | } 27 | .luna-carousel-indicators { 28 | display: none; 29 | } 30 | .luna-image-viewer { 31 | border: none; 32 | } 33 | &:hover { 34 | .toolbar { 35 | opacity: 1; 36 | } 37 | } 38 | } 39 | 40 | .no-scrollbar { 41 | overflow: hidden; 42 | } 43 | 44 | .image { 45 | width: 100%; 46 | height: 100%; 47 | position: relative; 48 | img { 49 | position: absolute; 50 | left: 0; 51 | top: 0; 52 | } 53 | .viewer { 54 | width: 100%; 55 | height: 100%; 56 | background: transparent; 57 | } 58 | } 59 | 60 | .title { 61 | color: #eee; 62 | background: rgba(0, 0, 0, 0.45); 63 | width: 100%; 64 | text-align: center; 65 | position: absolute; 66 | left: 0; 67 | bottom: 0; 68 | padding: 10px 40px; 69 | } 70 | 71 | .toolbar { 72 | position: absolute; 73 | opacity: 0; 74 | top: 0; 75 | left: 0; 76 | width: 100%; 77 | transition: opacity 0.3s; 78 | } 79 | -------------------------------------------------------------------------------- /src/gallery/test.js: -------------------------------------------------------------------------------- 1 | import Gallery from './index' 2 | import test from '../share/test' 3 | 4 | test('gallery', (container) => { 5 | const gallery = new Gallery(container) 6 | 7 | it('basic', function () { 8 | gallery.append('https://luna.liriliri.io/pic1.png', 'pic1.png') 9 | gallery.append('https://luna.liriliri.io/pic2.png', 'pic2.png') 10 | gallery.append('https://luna.liriliri.io/pic3.png', 'pic3.png') 11 | gallery.append('https://luna.liriliri.io/pic4.png', 'pic4.png') 12 | gallery.show() 13 | }) 14 | 15 | return gallery 16 | }) 17 | -------------------------------------------------------------------------------- /src/icon-list/README.md: -------------------------------------------------------------------------------- 1 | # Luna Icon List 2 | 3 | Show list of icons and their names. 4 | 5 | ## Demo 6 | 7 | https://luna.liriliri.io/?path=/story/icon-list 8 | 9 | ## Install 10 | 11 | Add the following script and style to your page. 12 | 13 | ```html 14 | 15 | 16 | ``` 17 | 18 | You can also get it on npm. 19 | 20 | ```bash 21 | npm install luna-icon-list --save 22 | ``` 23 | 24 | ```javascript 25 | import 'luna-icon-list/luna-icon-list.css' 26 | import LunaIconList from 'luna-icon-list' 27 | ``` 28 | 29 | ## Usage 30 | 31 | ```javascript 32 | const iconList = new LunaIconList(container) 33 | iconList.setIcons([ 34 | { 35 | src: '/logo.png', 36 | name: 'Luna', 37 | }, 38 | ]) 39 | ``` 40 | 41 | ## Configuration 42 | 43 | * filter(string | RegExp | AnyFn): Icon filter. 44 | * selectable(boolean): Whether icon is selectable. 45 | * size(number): Icon size. 46 | 47 | ## Api 48 | 49 | ### append(data: IIcon): void 50 | 51 | Append icon. 52 | 53 | ### clear(): void 54 | 55 | Clear all icons. 56 | 57 | ### setIcons(icons: IIcon[]): void 58 | 59 | Set icons. 60 | 61 | ## Types 62 | 63 | ### IIcon 64 | 65 | -------------------------------------------------------------------------------- /src/icon-list/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "icon-list", 3 | "version": "0.2.5", 4 | "description": "Show list of icons and their names", 5 | "luna": { 6 | "react": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/icon-list/test.js: -------------------------------------------------------------------------------- 1 | import IconList from './index' 2 | import test from '../share/test' 3 | 4 | test('icon-list', (container) => { 5 | it('basic', function () { 6 | const iconList = new IconList(container, { 7 | size: 64, 8 | }) 9 | iconList.setIcons([ 10 | { 11 | src: '/logo.png', 12 | name: 'Luna', 13 | }, 14 | ]) 15 | }) 16 | }) 17 | -------------------------------------------------------------------------------- /src/image-list/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "image-list", 3 | "version": "0.3.4", 4 | "description": "Show list of images", 5 | "luna": { 6 | "vue": true, 7 | "dependencies": [ 8 | "gallery" 9 | ] 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/image-list/style.scss: -------------------------------------------------------------------------------- 1 | @use '../share/mixin' as mixin; 2 | @use '../share/theme' as theme; 3 | 4 | .luna-image-list { 5 | @include mixin.component(); 6 | & { 7 | background-color: transparent; 8 | } 9 | &.theme-dark { 10 | background-color: transparent; 11 | } 12 | } 13 | 14 | .images { 15 | display: flex; 16 | flex-wrap: wrap; 17 | &::after { 18 | content: ''; 19 | flex-grow: 1000; 20 | } 21 | &.no-title { 22 | .title { 23 | display: none; 24 | } 25 | } 26 | } 27 | 28 | .item { 29 | flex-grow: 1; 30 | flex-direction: column; 31 | min-width: 0; 32 | display: inline-flex; 33 | cursor: pointer; 34 | &:hover { 35 | .image { 36 | box-shadow: theme.$box-shadow; 37 | } 38 | } 39 | } 40 | 41 | .image { 42 | width: 100%; 43 | border-radius: #{theme.$border-radius-l-g}px; 44 | overflow: hidden; 45 | background-color: theme.$color-bg-container; 46 | border: 1px solid theme.$color-border; 47 | img { 48 | object-fit: cover; 49 | width: 100%; 50 | height: 100%; 51 | } 52 | } 53 | 54 | .title { 55 | height: 20px; 56 | line-height: 20px; 57 | white-space: nowrap; 58 | text-overflow: ellipsis; 59 | overflow: hidden; 60 | text-align: center; 61 | } 62 | 63 | .theme-dark { 64 | .image { 65 | background-color: theme.$color-bg-container-dark; 66 | border-color: theme.$color-border-dark; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/image-list/test.js: -------------------------------------------------------------------------------- 1 | import ImageList from './index' 2 | import test from '../share/test' 3 | 4 | test('image-list', (container) => { 5 | const imageList = new ImageList(container) 6 | 7 | it('basic', function () { 8 | imageList.append('https://luna.liriliri.io/pic1.png', 'pic1.png') 9 | imageList.append('https://luna.liriliri.io/pic2.png', 'pic2.png') 10 | imageList.append('https://luna.liriliri.io/pic3.png', 'pic3.png') 11 | imageList.append('https://luna.liriliri.io/pic4.png', 'pic4.png') 12 | }) 13 | 14 | return imageList 15 | }) 16 | -------------------------------------------------------------------------------- /src/image-viewer/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.1.0 (24 Mar 2025) 2 | 3 | * feat: support passing canvas as an image 4 | 5 | ## 1.0.0 (9 Mar 2025) 6 | 7 | * fix: react theme 8 | -------------------------------------------------------------------------------- /src/image-viewer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "image-viewer", 3 | "version": "1.1.0", 4 | "description": "Single image viewer", 5 | "luna": { 6 | "react": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/image-viewer/react.tsx: -------------------------------------------------------------------------------- 1 | import { CSSProperties, FC, useEffect, useRef } from 'react' 2 | import ImageViewer, { IOptions } from './index' 3 | import each from 'licia/each' 4 | import { useOption } from '../share/hooks' 5 | 6 | interface IImageViewerProps extends IOptions { 7 | style?: CSSProperties 8 | className?: string 9 | onCreate?: (imageViewer: ImageViewer) => void 10 | } 11 | 12 | const LunaImageViewer: FC = (props) => { 13 | const imageViewerRef = useRef(null) 14 | const imageViewer = useRef() 15 | 16 | useEffect(() => { 17 | imageViewer.current = new ImageViewer(imageViewerRef.current!, { 18 | theme: props.theme, 19 | image: props.image, 20 | initialCoverage: props.initialCoverage, 21 | zoomOnWheel: props.zoomOnWheel, 22 | }) 23 | props.onCreate && props.onCreate(imageViewer.current) 24 | 25 | return () => imageViewer.current?.destroy() 26 | }, []) 27 | 28 | each(['theme', 'image', 'zoomOnWheel'], (key: keyof IImageViewerProps) => { 29 | useOption(imageViewer, key, props[key]) 30 | }) 31 | 32 | return ( 33 |
38 | ) 39 | } 40 | 41 | export default LunaImageViewer 42 | -------------------------------------------------------------------------------- /src/image-viewer/style.scss: -------------------------------------------------------------------------------- 1 | @use '../share/mixin' as *; 2 | 3 | .luna-image-viewer { 4 | overflow: hidden; 5 | touch-action: none; 6 | border: 1px solid; 7 | position: relative; 8 | @include component(); 9 | } 10 | 11 | .image { 12 | height: auto; 13 | margin: 15px auto; 14 | width: auto; 15 | display: block; 16 | } 17 | 18 | .ratio { 19 | background-color: rgba(0, 0, 0, 80%); 20 | border-radius: 10px; 21 | color: #fff; 22 | font-size: 12px; 23 | height: 20px; 24 | left: 50%; 25 | line-height: 20px; 26 | margin-left: -25px; 27 | margin-top: -10px; 28 | position: absolute; 29 | text-align: center; 30 | top: 50%; 31 | width: 50px; 32 | opacity: 0; 33 | transition: opacity 0.3s; 34 | &.show { 35 | opacity: 1; 36 | } 37 | } 38 | 39 | .image-transition { 40 | transition: all 0.3s; 41 | } 42 | 43 | @each $theme in ('light', 'dark') { 44 | .theme-#{$theme} { 45 | @include theme-var(border-color, color-border, $theme); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/image-viewer/test.js: -------------------------------------------------------------------------------- 1 | import ImageViewer from './index' 2 | import test from '../share/test' 3 | 4 | test('image-viewer', (container) => { 5 | const imageViewer = new ImageViewer(container, { 6 | image: 'https://luna.liriliri.io/wallpaper.png', 7 | }) 8 | it('basic', function () { 9 | imageViewer.zoom(0.1) 10 | }) 11 | }) 12 | -------------------------------------------------------------------------------- /src/json-editor/README.md: -------------------------------------------------------------------------------- 1 | # Luna Json Editor 2 | 3 | JSON editor. 4 | 5 | ## Demo 6 | 7 | https://luna.liriliri.io/?path=/story/json-editor 8 | 9 | ## Install 10 | 11 | Add the following script and style to your page. 12 | 13 | ```html 14 | 15 | 16 | ``` 17 | 18 | You can also get it on npm. 19 | 20 | ```bash 21 | npm install luna-json-editor --save 22 | ``` 23 | 24 | ```javascript 25 | import 'luna-json-editor/luna-json-editor.css' 26 | import LunaJsonEditor from 'luna-json-editor' 27 | ``` 28 | 29 | ## Usage 30 | 31 | ```javascript 32 | const container = document.getElementById('container') 33 | const jsonEditor = new LunaJsonEditor(container, { 34 | name: 'luna', 35 | value: { 36 | a: true, 37 | }, 38 | nameEditable: false, 39 | }) 40 | jsonEditor.expand(true) 41 | ``` 42 | 43 | ## Configuration 44 | 45 | * enableDelete(boolean): Enable deletion. 46 | * enableInsert(boolean): Enable insertion. 47 | * name(any): Object name. 48 | * nameEditable(boolean): Is name editable. 49 | * showName(boolean): Show object name or not. 50 | * valueEditable(boolean): Is value editable. 51 | 52 | ## Api 53 | 54 | ### expand(recursive?: boolean): void 55 | 56 | Expand object. 57 | -------------------------------------------------------------------------------- /src/json-editor/icon.json: -------------------------------------------------------------------------------- 1 | ["caret-down.svg", "caret-right.svg", "add.svg", "delete.svg"] 2 | -------------------------------------------------------------------------------- /src/json-editor/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "json-editor", 3 | "version": "0.1.0", 4 | "description": "JSON editor", 5 | "luna": { 6 | "icon": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/json-editor/test.js: -------------------------------------------------------------------------------- 1 | import JsonEditor from './index' 2 | import test from '../share/test' 3 | 4 | test('json-editor', (container) => { 5 | const jsonEditor = new JsonEditor(container, { 6 | name: 'luna', 7 | value: { 8 | a: true, 9 | }, 10 | nameEditable: false, 11 | }) 12 | 13 | it('basic', function () { 14 | jsonEditor.expand(true) 15 | 16 | const $container = $(container) 17 | expect($container.children('.luna-json-editor-name').text()).to.equal( 18 | 'luna' 19 | ) 20 | }) 21 | 22 | return jsonEditor 23 | }) 24 | -------------------------------------------------------------------------------- /src/keyboard/README.md: -------------------------------------------------------------------------------- 1 | # Luna Keyboard 2 | 3 | Virtual keyboard. 4 | 5 | ## Demo 6 | 7 | https://luna.liriliri.io/?path=/story/keyboard 8 | 9 | ## Install 10 | 11 | Add the following script and style to your page. 12 | 13 | ```html 14 | 15 | 16 | ``` 17 | 18 | You can also get it on npm. 19 | 20 | ```bash 21 | npm install luna-keyboard --save 22 | ``` 23 | 24 | ```javascript 25 | import 'luna-keyboard/luna-keyboard.css' 26 | import LunaKeyboard from 'luna-keyboard' 27 | ``` 28 | 29 | ## Usage 30 | 31 | ```javascript 32 | const textarea = document.getElementById('textarea') 33 | const container = document.getElementById('container') 34 | const keyboard = new LunaKeyboard(container) 35 | keyboard.on('change', (input) => { 36 | textarea.value = input 37 | }) 38 | textarea.addEventListener('input', (event) => { 39 | keyboard.setInput(event.target.value) 40 | }) 41 | ``` 42 | 43 | ## Api 44 | 45 | ### setInput(input: string): void 46 | 47 | Set input. 48 | -------------------------------------------------------------------------------- /src/keyboard/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "keyboard", 3 | "version": "0.1.0", 4 | "description": "Virtual keyboard" 5 | } 6 | -------------------------------------------------------------------------------- /src/keyboard/story.js: -------------------------------------------------------------------------------- 1 | import 'luna-keyboard.css' 2 | import Keyboard from 'luna-keyboard.js' 3 | import readme from './README.md' 4 | import story from '../share/story' 5 | import h from 'licia/h' 6 | import $ from 'licia/$' 7 | 8 | const def = story( 9 | 'keyboard', 10 | (wrapper) => { 11 | $(wrapper).html('').css({ 12 | maxWidth: 800, 13 | margin: '0 auto', 14 | fontSize: 0, 15 | }) 16 | 17 | const textarea = h('textarea', { 18 | placeholder: 'Tap to start', 19 | }) 20 | $(textarea).css({ 21 | width: '100%', 22 | background: '#2e2e2e', 23 | border: 'none', 24 | boxSizing: 'border-box', 25 | outline: 'none', 26 | borderRadius: '4px 4px 0 0', 27 | height: 134, 28 | fontSize: '18px', 29 | padding: 20, 30 | resize: 'vertical', 31 | color: '#fff', 32 | }) 33 | wrapper.appendChild(textarea) 34 | 35 | const keyboardContainer = h('div') 36 | wrapper.appendChild(keyboardContainer) 37 | 38 | const keyboard = new Keyboard(keyboardContainer) 39 | keyboard.on('change', (input) => { 40 | textarea.value = input 41 | }) 42 | textarea.addEventListener('input', (event) => { 43 | keyboard.setInput(event.target.value) 44 | }) 45 | 46 | return keyboard 47 | }, 48 | { 49 | readme, 50 | story: __STORY__, 51 | } 52 | ) 53 | 54 | export default def 55 | 56 | export const { keyboard } = def 57 | -------------------------------------------------------------------------------- /src/keyboard/test.js: -------------------------------------------------------------------------------- 1 | import Keyboard from './index' 2 | import test from '../share/test' 3 | 4 | test('keyboard', (container) => { 5 | const keyboard = new Keyboard(container) 6 | it('basic', function () { 7 | keyboard.setInput('test') 8 | }) 9 | }) 10 | -------------------------------------------------------------------------------- /src/log/README.md: -------------------------------------------------------------------------------- 1 | # Luna Log 2 | 3 | Terminal log viewer. 4 | 5 | ## Demo 6 | 7 | https://luna.liriliri.io/?path=/story/log 8 | 9 | ## Install 10 | 11 | Add the following script and style to your page. 12 | 13 | ```html 14 | 15 | 16 | 17 | ``` 18 | 19 | You can also get it on npm. 20 | 21 | ```bash 22 | npm install luna-log luna-text-viewer --save 23 | ``` 24 | 25 | ```javascript 26 | import 'luna-text-viewer/luna-text-viewer.css' 27 | import LunaLog from 'luna-log' 28 | ``` 29 | 30 | ## Usage 31 | 32 | ```javascript 33 | const log = new LunaLog(container) 34 | log.setOption({ 35 | log: 'npm install', 36 | }) 37 | ``` 38 | 39 | ## Configuration 40 | 41 | * log(string): Log to display. 42 | * maxHeight(number): Max viewer height. 43 | * wrapLongLines(boolean): Wrap lone lines. 44 | 45 | ## Api 46 | 47 | ### append(log: string): void 48 | 49 | Append log. 50 | -------------------------------------------------------------------------------- /src/log/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "log", 3 | "version": "0.1.0", 4 | "description": "Terminal log viewer", 5 | "luna": { 6 | "dependencies": [ 7 | "text-viewer" 8 | ], 9 | "style": false 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/log/react.tsx: -------------------------------------------------------------------------------- 1 | import { FC, MutableRefObject, useEffect, useRef } from 'react' 2 | import Log from './index' 3 | 4 | interface ILogProps { 5 | log?: string 6 | wrapLongLines?: boolean 7 | maxHeight?: number 8 | onCreate?: (log: Log) => void 9 | } 10 | 11 | const LunaLog: FC = (props) => { 12 | const logRef = useRef(null) 13 | const log = useRef() 14 | 15 | useEffect(() => { 16 | log.current = new Log(logRef.current!, { 17 | log: props.log, 18 | wrapLongLines: props.wrapLongLines, 19 | maxHeight: props.maxHeight, 20 | }) 21 | props.onCreate && props.onCreate(log.current) 22 | 23 | return () => log.current?.destroy() 24 | }, []) 25 | 26 | useEffect(() => setOption(log, 'log', props.log), [props.log]) 27 | useEffect( 28 | () => setOption(log, 'wrapLongLines', props.wrapLongLines), 29 | [props.wrapLongLines] 30 | ) 31 | useEffect( 32 | () => setOption(log, 'maxHeight', props.maxHeight), 33 | [props.maxHeight] 34 | ) 35 | 36 | return
37 | } 38 | 39 | function setOption( 40 | log: MutableRefObject, 41 | name: string, 42 | val: any 43 | ) { 44 | if (log.current) { 45 | log.current.setOption(name, val) 46 | } 47 | } 48 | 49 | export default LunaLog 50 | -------------------------------------------------------------------------------- /src/log/test.js: -------------------------------------------------------------------------------- 1 | import Log from './index' 2 | import test from '../share/test' 3 | 4 | test('log', (container) => { 5 | const log = new Log(container) 6 | 7 | it('basic', () => { 8 | log.setOption({ 9 | log: 'npm install', 10 | }) 11 | }) 12 | 13 | return log 14 | }) 15 | -------------------------------------------------------------------------------- /src/logcat/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "logcat", 3 | "version": "0.6.3", 4 | "description": "Android logcat viewer", 5 | "luna": { 6 | "react": true, 7 | "dependencies": [ 8 | "virtual-list" 9 | ] 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/logcat/react.tsx: -------------------------------------------------------------------------------- 1 | import { CSSProperties, FC, useEffect, useRef } from 'react' 2 | import each from 'licia/each' 3 | import Logcat, { IOptions, IEntry } from './index' 4 | import { useEvent, useOption, usePrevious } from '../share/hooks' 5 | 6 | interface IILogcatProps extends IOptions { 7 | style?: CSSProperties 8 | className?: string 9 | onContextMenu?: (e: PointerEvent, entry: IEntry) => void 10 | onCreate?: (logcat: Logcat) => void 11 | } 12 | 13 | const LunaLogcat: FC = (props) => { 14 | const logcatRef = useRef(null) 15 | const logcat = useRef() 16 | const prevProps = usePrevious(props) 17 | 18 | useEffect(() => { 19 | const { theme, maxNum, wrapLongLines, filter, entries, view } = props 20 | 21 | logcat.current = new Logcat(logcatRef.current!, { 22 | theme, 23 | filter, 24 | maxNum, 25 | wrapLongLines, 26 | entries, 27 | view, 28 | }) 29 | props.onCreate && props.onCreate(logcat.current) 30 | 31 | return () => logcat.current?.destroy() 32 | }, []) 33 | 34 | useEvent( 35 | logcat, 36 | 'contextmenu', 37 | prevProps?.onContextMenu, 38 | props.onContextMenu 39 | ) 40 | 41 | each( 42 | ['theme', 'filter', 'maxNum', 'wrapLongLines', 'view'], 43 | (key: keyof IOptions) => { 44 | useOption(logcat, key, props[key]) 45 | } 46 | ) 47 | 48 | return ( 49 |
54 | ) 55 | } 56 | 57 | export default LunaLogcat 58 | -------------------------------------------------------------------------------- /src/logcat/test.js: -------------------------------------------------------------------------------- 1 | import Logcat from './index' 2 | import test from '../share/test' 3 | 4 | test('log', (container) => { 5 | const logcat = new Logcat(container) 6 | 7 | it('basic', () => { 8 | logcat.append({ 9 | date: '2024-10-28T07:21:37.452Z', 10 | pid: 31332, 11 | tid: 17073, 12 | priority: 5, 13 | tag: 'System.err', 14 | message: 15 | 'java.lang.NoSuchMethodException: android.view.IWindowManager$Stub$Proxy.getRotation []', 16 | package: 'com.example', 17 | }) 18 | }) 19 | 20 | return logcat 21 | }) 22 | -------------------------------------------------------------------------------- /src/lrc-player/README.md: -------------------------------------------------------------------------------- 1 | # Luna Lrc Player 2 | 3 | Play lyrics in LRC format. 4 | 5 | ## Demo 6 | 7 | https://luna.liriliri.io/?path=/story/lrc-player 8 | 9 | ## Install 10 | 11 | Add the following script and style to your page. 12 | 13 | ```html 14 | 15 | 16 | ``` 17 | 18 | You can also get it on npm. 19 | 20 | ```bash 21 | npm install luna-lrc-player --save 22 | ``` 23 | 24 | ```javascript 25 | import 'luna-lrc-player/luna-lrc-player.css' 26 | import LunaLrcPlayer from 'luna-lrc-player' 27 | ``` 28 | -------------------------------------------------------------------------------- /src/lrc-player/index.ts: -------------------------------------------------------------------------------- 1 | import Component from '../share/Component' 2 | import { exportCjs } from '../share/util' 3 | 4 | /** 5 | * Play lyrics in LRC format. 6 | */ 7 | export default class LrcPlayer extends Component { 8 | constructor(container: HTMLElement) { 9 | super(container, { compName: 'lrc-player' }) 10 | } 11 | } 12 | 13 | if (typeof module !== 'undefined') { 14 | exportCjs(module, LrcPlayer) 15 | } 16 | -------------------------------------------------------------------------------- /src/lrc-player/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lrc-player", 3 | "version": "0.1.0", 4 | "description": "Play lyrics in LRC format", 5 | "luna": { 6 | "test": false 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/lrc-player/story.js: -------------------------------------------------------------------------------- 1 | import 'luna-lrc-player.css' 2 | import LrcPlayer from 'luna-lrc-player.js' 3 | import readme from './README.md' 4 | import story from '../share/story' 5 | 6 | const def = story( 7 | 'lrc-player', 8 | (container) => { 9 | const lrcPlayer = new LrcPlayer(container) 10 | 11 | return lrcPlayer 12 | }, 13 | { 14 | readme, 15 | story: __STORY__, 16 | } 17 | ) 18 | 19 | export default def 20 | 21 | export const { lrcPlayer } = def 22 | -------------------------------------------------------------------------------- /src/lrc-player/style.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liriliri/luna/09cfb448f0e8646fb790feb6ca03e6799e5dc697/src/lrc-player/style.scss -------------------------------------------------------------------------------- /src/markdown-editor/icon.json: -------------------------------------------------------------------------------- 1 | ["bold.svg", "italic.svg", "fullscreen.svg", "eye.svg"] 2 | -------------------------------------------------------------------------------- /src/markdown-editor/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "markdown-editor", 3 | "version": "0.1.0", 4 | "description": "Markdown editor with preview", 5 | "luna": { 6 | "dependencies": [ 7 | "markdown-viewer" 8 | ], 9 | "icon": true 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/markdown-editor/story.js: -------------------------------------------------------------------------------- 1 | import 'luna-markdown-editor.css' 2 | import MarkdownEditor from 'luna-markdown-editor.js' 3 | import story from '../share/story' 4 | import readme from './README.md' 5 | 6 | const def = story( 7 | 'markdown-editor', 8 | (container) => { 9 | const markdownEditor = new MarkdownEditor(container, { 10 | markdown: readme, 11 | }) 12 | 13 | return markdownEditor 14 | }, 15 | { 16 | readme, 17 | source: __STORY__, 18 | } 19 | ) 20 | 21 | export default def 22 | 23 | export const { markdownEditor } = def 24 | -------------------------------------------------------------------------------- /src/markdown-editor/test.js: -------------------------------------------------------------------------------- 1 | import MarkdownEditor from './index' 2 | import test from '../share/test' 3 | 4 | test('markdown-editor', (contianer) => { 5 | const markdownEditor = new MarkdownEditor(contianer) 6 | 7 | it('basic', () => { 8 | markdownEditor.markdown('# h1') 9 | expect(markdownEditor.markdown()).to.equal('# h1') 10 | }) 11 | 12 | return markdownEditor 13 | }) 14 | -------------------------------------------------------------------------------- /src/markdown-viewer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "markdown-viewer", 3 | "version": "0.1.0", 4 | "description": "Live markdown renderer", 5 | "dependencies": { 6 | "markdown-it": "^12.3.2" 7 | }, 8 | "devDependencies": { 9 | "@types/linkify-it": "^3.0.5", 10 | "@types/markdown-it": "^12.2.3" 11 | }, 12 | "luna": { 13 | "install": true, 14 | "dependencies": [ 15 | "syntax-highlighter", 16 | "gallery" 17 | ] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/markdown-viewer/story.js: -------------------------------------------------------------------------------- 1 | import 'luna-markdown-viewer.css' 2 | import MarkdownViewer from 'luna-markdown-viewer.js' 3 | import story from '../share/story' 4 | import readme from './README.md' 5 | import demo from './DEMO.md' 6 | import { text } from '@storybook/addon-knobs' 7 | 8 | const def = story( 9 | 'markdown-viewer', 10 | (container) => { 11 | const markdown = text('Markdown', readme + demo) 12 | 13 | const markdownViewer = new MarkdownViewer(container, { 14 | markdown, 15 | }) 16 | 17 | return markdownViewer 18 | }, 19 | { 20 | readme, 21 | source: __STORY__, 22 | } 23 | ) 24 | 25 | export default def 26 | 27 | export const { markdownViewer } = def 28 | -------------------------------------------------------------------------------- /src/markdown-viewer/test.js: -------------------------------------------------------------------------------- 1 | import MarkdownViewer from './index' 2 | import test from '../share/test' 3 | 4 | test('markdown-viewer', (container) => { 5 | const markdownViewer = new MarkdownViewer(container) 6 | 7 | it('basic', () => { 8 | markdownViewer.setOption('markdown', '# h1') 9 | }) 10 | 11 | return markdownViewer 12 | }) 13 | -------------------------------------------------------------------------------- /src/mask-editor/README.md: -------------------------------------------------------------------------------- 1 | # Luna Mask Editor 2 | 3 | Image mask editing. 4 | 5 | ## Demo 6 | 7 | https://luna.liriliri.io/?path=/story/mask-editor 8 | 9 | ## Install 10 | 11 | Add the following script and style to your page. 12 | 13 | ```html 14 | 15 | 16 | 17 | 18 | 19 | ``` 20 | 21 | You can also get it on npm. 22 | 23 | ```bash 24 | npm install luna-mask-editor luna-painter luna-toolbar --save 25 | ``` 26 | 27 | ```javascript 28 | import 'luna-toolbar/luna-toolbar.css' 29 | import 'luna-painter/luna-painter.css' 30 | import LunaMaskEditor from 'luna-mask-editor' 31 | ``` 32 | 33 | ## Usage 34 | 35 | ```javascript 36 | const container = document.getElementById('container') 37 | const maskEditor = new LunaMaskEditor(container) 38 | ``` 39 | 40 | ## Configuration 41 | 42 | * image(string): Image src. 43 | * mask(string): Mask src. 44 | 45 | ## Api 46 | 47 | ### getCanvas(): HTMLCanvasElement 48 | 49 | Get a canvas with mask drawn. 50 | -------------------------------------------------------------------------------- /src/mask-editor/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mask-editor", 3 | "version": "0.5.1", 4 | "description": "Image mask editing", 5 | "luna": { 6 | "dependencies": [ 7 | "painter" 8 | ], 9 | "style": false, 10 | "react": true 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/mask-editor/react.tsx: -------------------------------------------------------------------------------- 1 | import { CSSProperties, FC, useEffect, useRef } from 'react' 2 | import MaskEditor, { IOptions } from './index' 3 | import each from 'licia/each' 4 | import { useEvent, useOption, usePrevious } from '../share/hooks' 5 | 6 | interface IMaskEditorProps extends IOptions { 7 | style?: CSSProperties 8 | onChange?: (canvas: HTMLCanvasElement) => void 9 | onCreate?: (maskEditor: MaskEditor) => void 10 | } 11 | 12 | const LunaMaskEditor: FC = (props) => { 13 | const maskEditorRef = useRef(null) 14 | const maskEditor = useRef() 15 | const prevProps = usePrevious(props) 16 | 17 | useEffect(() => { 18 | const { image, mask, theme } = props 19 | maskEditor.current = new MaskEditor(maskEditorRef.current!, { 20 | image, 21 | mask, 22 | theme, 23 | }) 24 | props.onCreate && props.onCreate(maskEditor.current) 25 | 26 | return () => maskEditor.current?.destroy() 27 | }, []) 28 | 29 | useEvent( 30 | maskEditor, 31 | 'change', 32 | prevProps?.onChange, 33 | props.onChange 34 | ) 35 | each(['theme', 'image', 'mask'], (key: keyof IMaskEditorProps) => { 36 | useOption(maskEditor, key, props[key]) 37 | }) 38 | 39 | return
40 | } 41 | 42 | export default LunaMaskEditor 43 | -------------------------------------------------------------------------------- /src/mask-editor/test.js: -------------------------------------------------------------------------------- 1 | import MaskEditor from './index' 2 | import test from '../share/test' 3 | 4 | test('mask-editor', (container) => { 5 | const maskEditor = new MaskEditor(container, {}) 6 | it('basic', function () { 7 | maskEditor.getCanvas() 8 | }) 9 | }) 10 | -------------------------------------------------------------------------------- /src/menu-bar/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "menu-bar", 3 | "version": "0.1.0", 4 | "description": "Application menu bar", 5 | "luna": { 6 | "dependencies": [ 7 | "menu" 8 | ] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/menu-bar/story.js: -------------------------------------------------------------------------------- 1 | import 'luna-menu-bar.css' 2 | import MenuBar from 'luna-menu-bar.js' 3 | import story from '../share/story' 4 | import readme from './README.md' 5 | import { object } from '@storybook/addon-knobs' 6 | import cloneDeep from 'licia/cloneDeep' 7 | 8 | const def = story( 9 | 'menu-bar', 10 | (container) => { 11 | const template = object('Template', [ 12 | { 13 | label: 'File', 14 | submenu: [ 15 | { 16 | type: 'submenu', 17 | label: 'Open', 18 | submenu: [ 19 | { 20 | label: 'index.html', 21 | }, 22 | { 23 | label: 'example.js', 24 | }, 25 | ], 26 | }, 27 | { 28 | type: 'separator', 29 | }, 30 | { 31 | label: 'Exit', 32 | }, 33 | ], 34 | }, 35 | { 36 | label: 'Edit', 37 | submenu: [ 38 | { 39 | label: 'Cut', 40 | }, 41 | { 42 | label: 'Copy', 43 | }, 44 | { 45 | label: 'Paste', 46 | }, 47 | ], 48 | }, 49 | { 50 | label: 'Help', 51 | submenu: [ 52 | { 53 | label: 'About Luna', 54 | }, 55 | ], 56 | }, 57 | ]) 58 | const menuBar = MenuBar.build(container, cloneDeep(template)) 59 | 60 | return menuBar 61 | }, 62 | { 63 | readme, 64 | source: __STORY__, 65 | layout: 'fullscreen', 66 | } 67 | ) 68 | 69 | export default def 70 | 71 | export const { menuBar } = def 72 | -------------------------------------------------------------------------------- /src/menu-bar/style.scss: -------------------------------------------------------------------------------- 1 | @use '../share/mixin' as mixin; 2 | @use '../share/theme' as theme; 3 | 4 | .luna-menu-bar { 5 | border-bottom: 1px solid theme.$color-border; 6 | @include mixin.component(); 7 | } 8 | 9 | .menu-list { 10 | font-size: 0; 11 | @include mixin.clear-float(); 12 | } 13 | 14 | .menu-item { 15 | font-size: #{theme.$font-size-s-m}px; 16 | padding: 2px 6px; 17 | cursor: default; 18 | display: block; 19 | float: left; 20 | user-select: none; 21 | &:hover { 22 | color: theme.$color-text; 23 | background: theme.$color-fill-secondary; 24 | } 25 | } 26 | 27 | .theme-dark { 28 | border-color: theme.$color-border-dark; 29 | } 30 | -------------------------------------------------------------------------------- /src/menu-bar/test.js: -------------------------------------------------------------------------------- 1 | import MenuBar from './index' 2 | import test from '../share/test' 3 | 4 | test('menu', (container) => { 5 | it('basic', function () { 6 | MenuBar.build(container, [ 7 | { 8 | label: 'File', 9 | submenu: [ 10 | { 11 | type: 'submenu', 12 | label: 'Open', 13 | submenu: [ 14 | { 15 | label: 'index.html', 16 | }, 17 | { 18 | label: 'example.js', 19 | }, 20 | ], 21 | }, 22 | { 23 | type: 'separator', 24 | }, 25 | { 26 | label: 'Exit', 27 | }, 28 | ], 29 | }, 30 | ]) 31 | }) 32 | }) 33 | -------------------------------------------------------------------------------- /src/menu/README.md: -------------------------------------------------------------------------------- 1 | # Luna Menu 2 | 3 | Simple menu. 4 | 5 | ## Demo 6 | 7 | https://luna.liriliri.io/?path=/story/menu 8 | 9 | ## Install 10 | 11 | Add the following script and style to your page. 12 | 13 | ```html 14 | 15 | 16 | ``` 17 | 18 | You can also get it on npm. 19 | 20 | ```bash 21 | npm install luna-menu --save 22 | ``` 23 | 24 | ```javascript 25 | import 'luna-menu/luna-menu.css' 26 | import LunaMenu from 'luna-menu' 27 | ``` 28 | 29 | ## Usage 30 | 31 | ```javascript 32 | const menu = new LunaMenu() 33 | menu.append({ 34 | type: 'normal', 35 | label: 'New File', 36 | click() { 37 | console.log('New File clicked') 38 | } 39 | }) 40 | menu.show(0, 0) 41 | ``` 42 | 43 | ## Api 44 | 45 | ### append(options: IMenuItemOptions): void 46 | 47 | Append menu item. 48 | 49 | ### insert(pos: number, options: IMenuItemOptions): void 50 | 51 | Inert menu item to given position. 52 | 53 | ### show(x: number, y: number, parent?: LunaComponent): void 54 | 55 | Show menu at target position. 56 | 57 | ### static build(template: any[]): LunaComponent 58 | 59 | Create menu from template. 60 | 61 | ## Types 62 | 63 | ### IMenuItemOptions 64 | 65 | * label(string): Menu label. 66 | * submenu(LunaComponent): Sub menu. 67 | * type('normal' | 'separator' | 'submenu'): Menu type. 68 | * click(function): Click event handler. -------------------------------------------------------------------------------- /src/menu/icon.json: -------------------------------------------------------------------------------- 1 | ["caret-right.svg"] 2 | -------------------------------------------------------------------------------- /src/menu/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "menu", 3 | "version": "0.1.3", 4 | "description": "Simple menu", 5 | "luna": { 6 | "icon": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/menu/style.scss: -------------------------------------------------------------------------------- 1 | @use '../share/mixin' as mixin; 2 | @use '../share/theme' as theme; 3 | 4 | .luna-menu { 5 | position: absolute; 6 | padding: 4px 0; 7 | box-shadow: theme.$box-shadow; 8 | border: 1px solid theme.$color-border; 9 | padding: 4px 0; 10 | z-index: 4000; 11 | overflow-y: auto; 12 | @include mixin.component(); 13 | } 14 | 15 | .glass-pane { 16 | position: fixed; 17 | left: 0; 18 | top: 0; 19 | bottom: 0; 20 | right: 0; 21 | z-index: 3000; 22 | } 23 | 24 | .item { 25 | display: flex; 26 | border-top: 1px solid transparent; 27 | border-bottom: 1px solid transparent; 28 | padding: 2px 7px 2px 8px; 29 | white-space: nowrap; 30 | width: 100%; 31 | font-size: 12px; 32 | &.active { 33 | color: theme.$color-white; 34 | background: theme.$color-primary; 35 | } 36 | .icon-caret-right { 37 | margin-left: auto; 38 | padding-left: 10px; 39 | font-size: 12px; 40 | position: relative; 41 | top: 1px; 42 | } 43 | } 44 | 45 | .separator { 46 | height: 1px; 47 | background-color: theme.$color-border; 48 | margin: 5px 1px; 49 | } 50 | 51 | .theme-dark { 52 | border-color: theme.$color-border-dark; 53 | .separator { 54 | background-color: theme.$color-border-dark; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/modal/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.3.1 (26 Feb 2025) 2 | 3 | * refactor: theme 4 | 5 | ## 1.3.0 (3 Nov 2024) 6 | 7 | * feat: add auto theme 8 | 9 | ## 1.2.3 (10 May 2024) 10 | 11 | * fix: react onClose unable to update 12 | 13 | ## 1.2.2 (22 Apr 2024) 14 | 15 | * fix: title shrinked 16 | 17 | ## 1.2.1 (7 Oct 2023) 18 | 19 | * fix: alert promise 20 | 21 | ## 1.2.0 (25 Sep 2023) 22 | 23 | * feat: i18n 24 | 25 | ## 1.1.1 (5 Aug 2023) 26 | 27 | * fix: es import 28 | 29 | ## 1.1.0 (30 Jun 2023) 30 | 31 | * feat: react support 32 | 33 | ## 1.0.0 (9 Dec 2022) 34 | 35 | * feat: prompt confirm on enter keydown 36 | * feat: prompt input auto focus -------------------------------------------------------------------------------- /src/modal/icon.json: -------------------------------------------------------------------------------- 1 | ["close.svg"] 2 | -------------------------------------------------------------------------------- /src/modal/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "modal", 3 | "version": "1.3.1", 4 | "description": "Create modal dialogs", 5 | "luna": { 6 | "react": true, 7 | "icon": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/modal/test.js: -------------------------------------------------------------------------------- 1 | import Modal from './index' 2 | import test from '../share/test' 3 | 4 | test('modal', (container) => { 5 | const title = 'This is the Title' 6 | const modal = new Modal(container, { 7 | title, 8 | content: 'This is the content.', 9 | }) 10 | 11 | it('basic', function () { 12 | modal.show() 13 | expect($(container).html()).to.include(title) 14 | }) 15 | 16 | return modal 17 | }) 18 | -------------------------------------------------------------------------------- /src/music-player/README.md: -------------------------------------------------------------------------------- 1 | # Luna Music Player 2 | 3 | Music player with playlist support. 4 | 5 | ## Demo 6 | 7 | https://luna.liriliri.io/?path=/story/music-player 8 | 9 | ## Install 10 | 11 | Add the following script and style to your page. 12 | 13 | ```html 14 | 15 | 16 | ``` 17 | 18 | You can also get it on npm. 19 | 20 | ```bash 21 | npm install luna-music-player --save 22 | ``` 23 | 24 | ```javascript 25 | import 'luna-music-player/luna-music-player.css' 26 | import LunaMusicPlayer from 'luna-music-player' 27 | ``` 28 | 29 | ## Usage 30 | 31 | ```javascript 32 | const container = document.getElementById('container') 33 | const musicPlayer = new LunaMusicPlayer(container, { 34 | audio: { 35 | url: 'https://luna.liriliri.io/Get_along.mp3', 36 | cover: 'https://luna.liriliri.io/Get_along.jpg', 37 | title: 'Get Along', 38 | artist: '林原めぐみ', 39 | } 40 | }) 41 | musicPlayer.play() 42 | ``` 43 | 44 | ## Configuration 45 | 46 | * audio(IAudio | IAudio[]): Audio list. 47 | * listFolded(boolean): Whether list should folded at first. 48 | 49 | ## Types 50 | 51 | ### IAudio 52 | 53 | * artist(string): Audio artist. 54 | * cover(string): Audio cover. 55 | * title(string): Audio title. 56 | * url(string): Audio src. 57 | -------------------------------------------------------------------------------- /src/music-player/icon.json: -------------------------------------------------------------------------------- 1 | [ 2 | "play.svg", 3 | "pause.svg", 4 | "volume.svg", 5 | "volume-off.svg", 6 | "volume-down.svg", 7 | "file.svg", 8 | "list.svg", 9 | "loop-all.svg", 10 | "loop-off.svg", 11 | "loop-one.svg", 12 | "shuffle-disabled.svg", 13 | "shuffle.svg" 14 | ] 15 | -------------------------------------------------------------------------------- /src/music-player/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "music-player", 3 | "version": "0.1.0", 4 | "description": "Music player", 5 | "dependencies": { 6 | "jsmediatags": "3.9.3" 7 | }, 8 | "devDependencies": { 9 | "@types/jsmediatags": "^3.9.2" 10 | }, 11 | "luna": { 12 | "icon": true, 13 | "install": true 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/music-player/story.js: -------------------------------------------------------------------------------- 1 | import 'luna-music-player.css' 2 | import story from '../share/story' 3 | import $ from 'licia/$' 4 | import MusicPlayer from 'luna-music-player.js' 5 | import { object } from '@storybook/addon-knobs' 6 | import readme from './README.md' 7 | 8 | const def = story( 9 | 'music-player', 10 | (container) => { 11 | $(container).css({ 12 | width: 640, 13 | margin: '0 auto', 14 | maxWidth: '100%', 15 | }) 16 | 17 | const audio = object('Audio', [ 18 | { 19 | url: '/Get_along.mp3', 20 | cover: '/Get_along.jpg', 21 | title: 'Get Along', 22 | artist: '林原めぐみ', 23 | }, 24 | { 25 | url: '/Give_a_reason.mp3', 26 | cover: '/Give_a_reason.jpg', 27 | title: 'Give a Reason', 28 | artist: '林原めぐみ', 29 | }, 30 | ]) 31 | 32 | const musicPlayer = new MusicPlayer(container, { 33 | audio, 34 | }) 35 | 36 | return musicPlayer 37 | }, 38 | { 39 | readme, 40 | source: __STORY__, 41 | } 42 | ) 43 | 44 | export default def 45 | 46 | export const { musicPlayer } = def 47 | -------------------------------------------------------------------------------- /src/music-player/test.js: -------------------------------------------------------------------------------- 1 | import MusicPlayer from './index' 2 | import test from '../share/test' 3 | 4 | test('music-player', (container) => { 5 | const musicPlayer = new MusicPlayer(container, { 6 | audio: [], 7 | }) 8 | 9 | it('basic', function () { 10 | musicPlayer.play() 11 | }) 12 | 13 | return musicPlayer 14 | }) 15 | -------------------------------------------------------------------------------- /src/music-player/util.ts: -------------------------------------------------------------------------------- 1 | import splitPath from 'licia/splitPath' 2 | import contain from 'licia/contain' 3 | 4 | export function splitName(str: string) { 5 | const { name, ext } = splitPath(str) 6 | 7 | if (contain(name, ' - ')) { 8 | const parts = name.replace(ext, '').split(' - ') 9 | return { 10 | title: parts[1], 11 | artist: parts[0], 12 | } 13 | } 14 | 15 | return { 16 | title: name, 17 | artist: '', 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/music-visualizer/README.md: -------------------------------------------------------------------------------- 1 | # Luna Music Visualizer 2 | 3 | Music visualization. 4 | 5 | ## Demo 6 | 7 | https://luna.liriliri.io/?path=/story/music-visualizer 8 | 9 | ## Install 10 | 11 | Add the following script and style to your page. 12 | 13 | ```html 14 | 15 | 16 | ``` 17 | 18 | You can also get it on npm. 19 | 20 | ```bash 21 | npm install luna-music-visualizer --save 22 | ``` 23 | 24 | ```javascript 25 | import 'luna-music-visualizer/luna-music-visualizer.css' 26 | import LunaMusicVisualizer from 'luna-music-visualizer' 27 | ``` 28 | 29 | ## Configuration 30 | 31 | * audio(HTMLAudioElement): Html audio element. 32 | -------------------------------------------------------------------------------- /src/music-visualizer/icon.json: -------------------------------------------------------------------------------- 1 | ["fullscreen.svg", "step-forward.svg"] 2 | -------------------------------------------------------------------------------- /src/music-visualizer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "music-visualizer", 3 | "description": "Music visualization", 4 | "version": "0.1.0", 5 | "luna": { 6 | "icon": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/music-visualizer/style.scss: -------------------------------------------------------------------------------- 1 | @use '../share/mixin' as mixin; 2 | 3 | .luna-music-visualizer { 4 | background: #13242f; 5 | font-size: 0; 6 | position: relative; 7 | min-height: 150px; 8 | } 9 | 10 | @include mixin.controller('music-visualizer'); 11 | 12 | .canvas { 13 | width: 100%; 14 | height: 100%; 15 | } 16 | -------------------------------------------------------------------------------- /src/music-visualizer/test.js: -------------------------------------------------------------------------------- 1 | import MusicVisualizer from './index' 2 | import test from '../share/test' 3 | 4 | test('music-visualizer', (container) => { 5 | const musicVisualizer = new MusicVisualizer(container, { 6 | audio: document.createElement('audio'), 7 | }) 8 | 9 | it('basic', () => { 10 | musicVisualizer.setOption('image', '') 11 | }) 12 | 13 | return musicVisualizer 14 | }) 15 | -------------------------------------------------------------------------------- /src/notification/README.md: -------------------------------------------------------------------------------- 1 | # Luna Notification 2 | 3 | Show notifications. 4 | 5 | ## Demo 6 | 7 | https://luna.liriliri.io/?path=/story/notification 8 | 9 | ## Install 10 | 11 | Add the following script and style to your page. 12 | 13 | ```html 14 | 15 | 16 | ``` 17 | 18 | You can also get it on npm. 19 | 20 | ```bash 21 | npm install luna-notification --save 22 | ``` 23 | 24 | ```javascript 25 | import 'luna-notification/luna-notification.css' 26 | import LunaNotification from 'luna-notification' 27 | ``` 28 | 29 | ## Usage 30 | 31 | ```javascript 32 | const container = document.getElementById('container') 33 | const notification = new LunaNotification(container, { 34 | position: { 35 | x: 'left', 36 | y: 'top', 37 | }, 38 | }) 39 | notification.notify('luna', { 40 | duration: 2000, 41 | }) 42 | notification.dismissAll() 43 | ``` 44 | 45 | ## Configuration 46 | 47 | * duration(number): Default duration, 0 means infinite. 48 | * inline(boolean): Enable inline mode. 49 | * position(IPosition): Notification position. 50 | 51 | ## Api 52 | 53 | ### dismissAll(): void 54 | 55 | Dismiss all notifications. 56 | 57 | ### notify(content: string, options?: INotifyOptions): void 58 | 59 | Show notification. 60 | 61 | ## Types 62 | 63 | ### INotifyOptions 64 | 65 | * duration(number): Notification duration. 66 | * icon(string): Notification icon. 67 | 68 | ### IPosition 69 | 70 | * x('left' | 'center' | 'right'): X position. 71 | * y('top' | 'bottom'): Y position. 72 | -------------------------------------------------------------------------------- /src/notification/icon.json: -------------------------------------------------------------------------------- 1 | ["check.svg", "warn.svg", "error.svg", "info.svg"] 2 | -------------------------------------------------------------------------------- /src/notification/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "notification", 3 | "version": "0.3.3", 4 | "description": "Show notifications", 5 | "luna": { 6 | "icon": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/notification/test.js: -------------------------------------------------------------------------------- 1 | import Notification from './index' 2 | import test from '../share/test' 3 | 4 | test('notification', (container) => { 5 | const notification = new Notification(container, { 6 | position: { 7 | x: 'right', 8 | y: 'top', 9 | }, 10 | }) 11 | 12 | it('basic', () => { 13 | const $container = $(container) 14 | 15 | notification.notify('luna', { duration: 5000 }) 16 | expect($container.find('.luna-notification-item').length).to.equal(1) 17 | 18 | notification.dismissAll() 19 | expect($container.find('.luna-notification-item').length).to.equal(0) 20 | }) 21 | 22 | return notification 23 | }) 24 | -------------------------------------------------------------------------------- /src/object-viewer/README.md: -------------------------------------------------------------------------------- 1 | # Luna Object Viewer 2 | 3 | JavaScript object viewer, useful for building debugging tool. 4 | 5 | ## Demo 6 | 7 | https://luna.liriliri.io/?path=/story/object-viewer 8 | 9 | ## Install 10 | 11 | Add the following script and style to your page. 12 | 13 | ```html 14 | 15 | 16 | ``` 17 | 18 | You can also get it on npm. 19 | 20 | ```bash 21 | npm install luna-object-viewer --save 22 | ``` 23 | 24 | ```javascript 25 | import 'luna-object-viewer/luna-object-viewer.css' 26 | import LunaObjectViewer from 'luna-object-viewer' 27 | ``` 28 | 29 | ## Usage 30 | 31 | ```javascript 32 | const container = document.getElementById('container') 33 | const objectViewer = new LunaObjectViewer(container, { 34 | unenumerable: false, 35 | accessGetter: true, 36 | }) 37 | objectViewer.set(window.navigator) 38 | ``` 39 | 40 | ## Configuration 41 | 42 | * accessGetter(boolean): Access getter value. 43 | * object(any): JavaScript object to display. 44 | * prototype(boolean): Show prototype. 45 | * unenumerable(boolean): Show unenumerable properties. 46 | 47 | ## Api 48 | 49 | ### set(data: any): void 50 | 51 | Set the JavaScript object to display. 52 | -------------------------------------------------------------------------------- /src/object-viewer/Visitor.ts: -------------------------------------------------------------------------------- 1 | import extend from 'licia/extend' 2 | 3 | export default class Visitor { 4 | id: number 5 | visited: any[] 6 | constructor() { 7 | this.id = 0 8 | this.visited = [] 9 | } 10 | set(val: any, extra: any) { 11 | const { visited, id } = this 12 | const obj = { 13 | id, 14 | val, 15 | } 16 | extend(obj, extra) 17 | visited.push(obj) 18 | 19 | this.id++ 20 | 21 | return id 22 | } 23 | get(val: any) { 24 | const { visited } = this 25 | 26 | for (let i = 0, len = visited.length; i < len; i++) { 27 | const obj = visited[i] 28 | if (val === obj.val) return obj 29 | } 30 | 31 | return false 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/object-viewer/icon.json: -------------------------------------------------------------------------------- 1 | ["caret-down.svg", "caret-right.svg"] 2 | -------------------------------------------------------------------------------- /src/object-viewer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "object-viewer", 3 | "version": "0.3.2", 4 | "description": "JavaScript object viewer", 5 | "luna": { 6 | "icon": true, 7 | "react": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/object-viewer/react.tsx: -------------------------------------------------------------------------------- 1 | import { CSSProperties, FC, useEffect, useRef } from 'react' 2 | import ObjectViewer, { IOptions } from './index' 3 | import each from 'licia/each' 4 | import clone from 'licia/clone' 5 | import { useNonInitialEffect } from '../share/hooks' 6 | 7 | interface IObjectViewerProps extends IOptions { 8 | style?: CSSProperties 9 | className?: string 10 | } 11 | 12 | const LunaObjectViewer: FC = (props) => { 13 | const objectViewerRef = useRef(null) 14 | const objectViewer = useRef() 15 | 16 | useEffect(() => { 17 | objectViewer.current = new ObjectViewer( 18 | objectViewerRef.current!, 19 | clone(props) 20 | ) 21 | 22 | return () => objectViewer.current?.destroy() 23 | }, []) 24 | 25 | each( 26 | ['theme', 'object', 'prototype', 'unenumerable', 'accessGetter'], 27 | (key: keyof IObjectViewerProps) => { 28 | useNonInitialEffect(() => { 29 | if (objectViewer.current) { 30 | objectViewer.current.setOption(key, props[key]) 31 | } 32 | }, [props[key]]) 33 | } 34 | ) 35 | 36 | return ( 37 |
42 | ) 43 | } 44 | 45 | export default LunaObjectViewer 46 | -------------------------------------------------------------------------------- /src/object-viewer/util.ts: -------------------------------------------------------------------------------- 1 | import toStr from 'licia/toStr' 2 | import trim from 'licia/trim' 3 | import escape from 'licia/escape' 4 | 5 | export const encode = (val: any) => { 6 | return escape(toStr(val)) 7 | .replace(/\n/g, '↵') 8 | .replace(/\f|\r|\t/g, '') 9 | } 10 | 11 | export function getFnAbstract(str: string) { 12 | if (str.length > 500) str = str.slice(0, 500) + '...' 13 | 14 | return 'ƒ ' + trim(extractFnHead(str).replace('function', '')) 15 | } 16 | 17 | const regFnHead = /function(.*?)\((.*?)\)/ 18 | 19 | function extractFnHead(str: string) { 20 | const fnHead = str.match(regFnHead) 21 | 22 | if (fnHead) return fnHead[0] 23 | 24 | return str 25 | } 26 | -------------------------------------------------------------------------------- /src/otp-input/README.md: -------------------------------------------------------------------------------- 1 | # Luna Otp Input 2 | 3 | One time password input. 4 | 5 | ## Demo 6 | 7 | https://luna.liriliri.io/?path=/story/otp-input 8 | 9 | ## Install 10 | 11 | Add the following script and style to your page. 12 | 13 | ```html 14 | 15 | 16 | ``` 17 | 18 | You can also get it on npm. 19 | 20 | ```bash 21 | npm install luna-otp-input --save 22 | ``` 23 | 24 | ```javascript 25 | import 'luna-otp-input/luna-otp-input.css' 26 | import LunaOtpInput from 'luna-otp-input' 27 | ``` 28 | 29 | ## Usage 30 | 31 | ```javascript 32 | const otpInput = new OtpInput(container, { 33 | inputNum: 6 34 | }) 35 | otpInput.getValue() 36 | ``` 37 | 38 | ## Configuration 39 | 40 | * inputNum(number): Number of inputs. 41 | 42 | ## Api 43 | 44 | ### getValue(): string 45 | 46 | Get otp value. 47 | -------------------------------------------------------------------------------- /src/otp-input/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "otp-input", 3 | "version": "0.1.1", 4 | "description": "One time password input", 5 | "luna": { 6 | "react": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/otp-input/react.tsx: -------------------------------------------------------------------------------- 1 | import { CSSProperties, FC, useEffect, useRef } from 'react' 2 | import OtpInput, { IOptions } from './index' 3 | import { useEvent, useOption, usePrevious } from '../share/hooks' 4 | import each from 'licia/each' 5 | 6 | interface IOtpInputProps extends IOptions { 7 | style?: CSSProperties 8 | className?: string 9 | onChange?: (value: string) => void 10 | onComplete?: (value: string) => void 11 | } 12 | 13 | const LunaOtpInput: FC = (props) => { 14 | const otpInputRef = useRef(null) 15 | const otpInput = useRef() 16 | const prevProps = usePrevious(props) 17 | 18 | useEffect(() => { 19 | otpInput.current = new OtpInput(otpInputRef.current!, { 20 | theme: props.theme, 21 | inputNum: props.inputNum, 22 | }) 23 | 24 | return () => otpInput.current?.destroy() 25 | }, []) 26 | 27 | useEvent(otpInput, 'change', prevProps?.onChange, props.onChange) 28 | useEvent( 29 | otpInput, 30 | 'complete', 31 | prevProps?.onComplete, 32 | props.onComplete 33 | ) 34 | 35 | each(['theme'], (key: keyof IOtpInputProps) => { 36 | useOption(otpInput, key, props[key]) 37 | }) 38 | 39 | return ( 40 |
45 | ) 46 | } 47 | 48 | export default LunaOtpInput 49 | -------------------------------------------------------------------------------- /src/otp-input/style.scss: -------------------------------------------------------------------------------- 1 | @use '../share/mixin' as *; 2 | @use '../share/theme' as *; 3 | 4 | .luna-otp-input { 5 | display: flex; 6 | justify-content: space-between; 7 | width: 100%; 8 | @include component(true); 9 | } 10 | 11 | .luna-otp-input input { 12 | width: 48px; 13 | height: 48px; 14 | font-size: 24px; 15 | border-radius: #{$border-radius-s-m}px; 16 | text-align: center; 17 | border: 1px solid; 18 | } 19 | 20 | @each $theme in ('light', 'dark') { 21 | .theme-#{$theme} { 22 | input { 23 | @include theme-vars( 24 | ( 25 | border-color: color-border, 26 | background-color: color-bg-container, 27 | color: color-text, 28 | ), 29 | $theme 30 | ); 31 | &:focus { 32 | @include theme-var(outline-color, color-primary, $theme); 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/otp-input/test.js: -------------------------------------------------------------------------------- 1 | import OtpInput from './index' 2 | import test from '../share/test' 3 | 4 | test('otp-input', (container) => { 5 | const otpInput = new OtpInput(container, { 6 | inputNum: 6, 7 | }) 8 | 9 | it('basic', function () { 10 | expect($(container).find('input').length).to.equal(6) 11 | }) 12 | 13 | return otpInput 14 | }) 15 | -------------------------------------------------------------------------------- /src/painter/README.md: -------------------------------------------------------------------------------- 1 | # Luna Painter 2 | 3 | Simple drawing tool. 4 | 5 | ## Demo 6 | 7 | https://luna.liriliri.io/?path=/story/painter 8 | 9 | ## Install 10 | 11 | Add the following script and style to your page. 12 | 13 | ```html 14 | 15 | 16 | 17 | 18 | ``` 19 | 20 | You can also get it on npm. 21 | 22 | ```bash 23 | npm install luna-painter luna-toolbar --save 24 | ``` 25 | 26 | ```javascript 27 | import 'luna-toolbar/luna-toolbar.css' 28 | import 'luna-painter/luna-painter.css' 29 | import LunaPainter from 'luna-painter' 30 | ``` 31 | 32 | ## Usage 33 | 34 | ```javascript 35 | const container = document.getElementById('container') 36 | const painer = new LunaPainter(container) 37 | ``` 38 | 39 | ## Configuration 40 | 41 | * height(number): Canvas height. 42 | * tool(string): Initial used tool. 43 | * width(number): Canvas width. 44 | 45 | ## Api 46 | 47 | ### addLayer(): number 48 | 49 | Add layer. 50 | 51 | ### getActiveLayer(): Layer 52 | 53 | Get active layer. 54 | 55 | ### getCurrentToolName(): string 56 | 57 | Get current tool name. 58 | 59 | ### getTool(name: string): void | LunaComponent 60 | 61 | Get tool. 62 | 63 | ### useTool(name: string): void 64 | 65 | Use tool. 66 | -------------------------------------------------------------------------------- /src/painter/icon.json: -------------------------------------------------------------------------------- 1 | [ 2 | "zoom-out.svg", 3 | "zoom-in.svg", 4 | "brush.svg", 5 | "crosshair.svg", 6 | "eraser.svg", 7 | "eyedropper.svg", 8 | "hand.svg", 9 | "paint-bucket.svg", 10 | "pencil.svg", 11 | "reset-color.svg", 12 | "swap.svg", 13 | "zoom.svg" 14 | ] 15 | -------------------------------------------------------------------------------- /src/painter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "painter", 3 | "version": "0.5.0", 4 | "description": "Simple drawing tool", 5 | "luna": { 6 | "dependencies": [ 7 | "toolbar" 8 | ], 9 | "icon": true, 10 | "react": true 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/painter/react.tsx: -------------------------------------------------------------------------------- 1 | import { CSSProperties, FC, useEffect, useRef } from 'react' 2 | import Painter, { IOptions } from './index' 3 | import { useOption } from '../share/hooks' 4 | import clone from 'licia/clone' 5 | import each from 'licia/each' 6 | 7 | interface IPainterProps extends IOptions { 8 | style?: CSSProperties 9 | onCreate?: (painter: Painter) => void 10 | } 11 | 12 | const LunaPainter: FC = (props) => { 13 | const painterRef = useRef(null) 14 | const painter = useRef() 15 | 16 | useEffect(() => { 17 | painter.current = new Painter(painterRef.current!, clone(props)) 18 | props.onCreate && props.onCreate(painter.current) 19 | 20 | return () => painter.current?.destroy() 21 | }, []) 22 | 23 | each(['theme', 'width', 'height'], (key: keyof IPainterProps) => { 24 | useOption(painter, key, props[key]) 25 | }) 26 | 27 | return
28 | } 29 | 30 | export default LunaPainter 31 | -------------------------------------------------------------------------------- /src/painter/test.js: -------------------------------------------------------------------------------- 1 | import Painter from './index' 2 | import test from '../share/test' 3 | 4 | test('painter', (container) => { 5 | const painter = new Painter(container, { 6 | width: 512, 7 | height: 512, 8 | tool: 'brush', 9 | }) 10 | it('basic', function () { 11 | painter.renderCanvas() 12 | }) 13 | }) 14 | -------------------------------------------------------------------------------- /src/painter/tools/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Tool } from './Tool' 2 | export { default as Pencil } from './Pencil' 3 | export { default as Hand } from './Hand' 4 | export { default as Zoom } from './Zoom' 5 | export { default as Brush } from './Brush' 6 | export { default as PaintBucket } from './PaintBucket' 7 | export { default as Eraser } from './Eraser' 8 | export { default as Eyedropper } from './Eyedropper' 9 | -------------------------------------------------------------------------------- /src/painter/util.ts: -------------------------------------------------------------------------------- 1 | export function duplicateCanvas( 2 | canvas: HTMLCanvasElement, 3 | includingContent = false 4 | ) { 5 | const result = document.createElement('canvas') 6 | result.width = canvas.width 7 | result.height = canvas.height 8 | if (includingContent) { 9 | result.getContext('2d')!.drawImage(canvas, 0, 0) 10 | } 11 | return result 12 | } 13 | 14 | const ratio3 = 255 / Math.sqrt(255 * 255 * 3) 15 | const ratio4 = 255 / Math.sqrt(255 * 255 * 4) 16 | 17 | export function colorDistance(color1: number[], color2: number[]) { 18 | const r = Math.abs(color1[0] - color2[0]) 19 | const g = Math.abs(color1[1] - color2[1]) 20 | const b = Math.abs(color1[2] - color2[2]) 21 | if (color1.length === 4 && color2.length === 4) { 22 | const a = Math.abs(color1[3] - color2[3]) 23 | if (a !== 0) { 24 | return Math.sqrt(r * r + g * g + b * b + a * a) * ratio4 25 | } 26 | } 27 | return Math.sqrt(r * r + g * g + b * b) * ratio3 28 | } 29 | -------------------------------------------------------------------------------- /src/performance-monitor/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.2.2 (2 Mar 2025) 2 | 3 | * refactor: theme 4 | 5 | ## 1.2.1 (28 Nov 2024) 6 | 7 | * fix: value disappear 8 | 9 | ## 1.2.0 (28 Nov 2024) 10 | 11 | * feat: update title 12 | 13 | ## 1.1.0 (27 Nov 2024) 14 | 15 | * feat: chart height option 16 | * fix: render error if dpr is changed 17 | 18 | ## 1.0.0 (8 Oct 2024) 19 | 20 | * feat: use border instead of box-shadow 21 | -------------------------------------------------------------------------------- /src/performance-monitor/README.md: -------------------------------------------------------------------------------- 1 | # Luna Performance Monitor 2 | 3 | Realtime counter used for displaying cpu, fps metrics. 4 | 5 | ## Demo 6 | 7 | https://luna.liriliri.io/?path=/story/performance-monitor 8 | 9 | ## Install 10 | 11 | Add the following script and style to your page. 12 | 13 | ```html 14 | 15 | 16 | ``` 17 | 18 | You can also get it on npm. 19 | 20 | ```bash 21 | npm install luna-performance-monitor --save 22 | ``` 23 | 24 | ```javascript 25 | import 'luna-performance-monitor/luna-performance-monitor.css' 26 | import LunaPerformanceMonitor from 'luna-performance-monitor' 27 | ``` 28 | 29 | ## Usage 30 | 31 | ```javascript 32 | const memoryMonitor = new PerformanceMonitor(container, { 33 | title: 'Used JS heap size', 34 | unit: 'MB', 35 | color: '#614d82', 36 | smooth: false, 37 | data() { 38 | return (performance.memory.usedJSHeapSize / 1024 / 1024).toFixed(1) 39 | }, 40 | }) 41 | memoryMonitor.start() 42 | ``` 43 | 44 | ## Configuration 45 | 46 | * color(string): Line color. 47 | * data(Fn): Data source provider, a number should be returned. 48 | * height(number): Chart height. 49 | * max(number): Maximum value. 50 | * smooth(boolean): Smooth lines or not. 51 | * title(string): Monitor title. 52 | * unit(string): Unit of the value. 53 | 54 | ## Api 55 | 56 | ### start(): void 57 | 58 | Start monitoring. 59 | 60 | ### stop(): void 61 | 62 | Stop monitoring. 63 | -------------------------------------------------------------------------------- /src/performance-monitor/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "performance-monitor", 3 | "version": "1.2.2", 4 | "description": "Realtime counter used for displaying cpu, fps metrics", 5 | "luna": { 6 | "react": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/performance-monitor/react.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useEffect, useRef } from 'react' 2 | import PerformanceMonitor, { IOptions } from './index' 3 | import { useOption } from '../share/hooks' 4 | import each from 'licia/each' 5 | import clone from 'licia/clone' 6 | 7 | const LunaPerformanceMonitor: FC = (props) => { 8 | const performanceMonitorRef = useRef(null) 9 | const performanceMonitor = useRef() 10 | 11 | useEffect(() => { 12 | performanceMonitor.current = new PerformanceMonitor( 13 | performanceMonitorRef.current!, 14 | clone(props) 15 | ) 16 | performanceMonitor.current.start() 17 | 18 | return () => performanceMonitor.current?.destroy() 19 | }, []) 20 | 21 | each(['theme', 'color', 'height', 'title'], (key: keyof IOptions) => { 22 | useOption(performanceMonitor, key, props[key]) 23 | }) 24 | 25 | return
26 | } 27 | 28 | export default LunaPerformanceMonitor 29 | -------------------------------------------------------------------------------- /src/performance-monitor/style.scss: -------------------------------------------------------------------------------- 1 | @use '../share/mixin' as *; 2 | @use '../share/theme' as *; 3 | 4 | .luna-performance-monitor { 5 | border: 1px solid; 6 | width: 100%; 7 | padding: 5px; 8 | @include component(); 9 | } 10 | 11 | .header { 12 | font-size: #{$font-size}px; 13 | margin-bottom: 5px; 14 | color: $color-text; 15 | } 16 | 17 | .value { 18 | float: right; 19 | } 20 | 21 | .chart { 22 | box-sizing: border-box; 23 | border: 1px solid; 24 | width: 100%; 25 | } 26 | 27 | @each $theme in ('light', 'dark') { 28 | .theme-#{$theme} { 29 | @include theme-var(border-color, color-border, $theme); 30 | .title { 31 | @include theme-var(color, color-text, $theme); 32 | } 33 | .chart { 34 | @include theme-var(border-color, color-border, $theme); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/performance-monitor/test.js: -------------------------------------------------------------------------------- 1 | import PerformanceMonitor from './index' 2 | import test from '../share/test' 3 | 4 | test('performance-monitor', (container) => { 5 | const performanceMonitor = new PerformanceMonitor(container, { 6 | title: 'Test', 7 | data: () => 1, 8 | }) 9 | performanceMonitor.start() 10 | 11 | it('basic', function (done) { 12 | const $title = $(container).find(performanceMonitor.c('.title')) 13 | setTimeout(() => { 14 | expect($title.text()).to.equal('Test') 15 | done() 16 | }, 20) 17 | }) 18 | 19 | return performanceMonitor 20 | }) 21 | -------------------------------------------------------------------------------- /src/qrcode-generator/README.md: -------------------------------------------------------------------------------- 1 | # Luna Qrcode Generator 2 | 3 | QR code generator. 4 | 5 | ## Demo 6 | 7 | https://luna.liriliri.io/?path=/story/qrcode-generator 8 | 9 | ## Install 10 | 11 | Add the following script and style to your page. 12 | 13 | ```html 14 | 15 | 16 | ``` 17 | 18 | You can also get it on npm. 19 | 20 | ```bash 21 | npm install luna-qrcode-generator --save 22 | ``` 23 | 24 | ```javascript 25 | import 'luna-qrcode-generator/luna-qrcode-generator.css' 26 | import LunaQrcodeGenerator from 'luna-qrcode-generator' 27 | ``` 28 | -------------------------------------------------------------------------------- /src/qrcode-generator/index.ts: -------------------------------------------------------------------------------- 1 | import stripIndent from 'licia/stripIndent' 2 | import { exportCjs } from '../share/util' 3 | import Component from '../share/Component' 4 | import QRCode from 'qrcode' 5 | 6 | /** 7 | * QR code generator. 8 | */ 9 | export default class QrcodeGenerator extends Component { 10 | private canvas: HTMLCanvasElement 11 | constructor(container: HTMLInputElement) { 12 | super(container, { compName: 'qrcode-generator' }) 13 | 14 | this.initTpl() 15 | const $canvas = this.find('.qrcode').find('canvas') 16 | this.canvas = $canvas.get(0) as HTMLCanvasElement 17 | 18 | this.generate() 19 | } 20 | private generate() { 21 | QRCode.toCanvas(this.canvas, 'test') 22 | } 23 | private initTpl() { 24 | this.$container.html( 25 | this.c(stripIndent` 26 |
27 |
28 |
29 | 30 |
31 |
32 |
33 |
34 | 35 |
36 |
37 |
38 | `) 39 | ) 40 | } 41 | } 42 | 43 | if (typeof module !== 'undefined') { 44 | exportCjs(module, QrcodeGenerator) 45 | } 46 | -------------------------------------------------------------------------------- /src/qrcode-generator/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "qrcode-generator", 3 | "version": "0.1.0", 4 | "description": "QR code generator", 5 | "dependencies": { 6 | "qrcode": "^1.5.1" 7 | }, 8 | "devDependencies": { 9 | "@types/qrcode": "^1.5.0" 10 | }, 11 | "luna": { 12 | "install": true, 13 | "test": false 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/qrcode-generator/story.js: -------------------------------------------------------------------------------- 1 | import 'luna-qrcode-generator.css' 2 | import h from 'licia/h' 3 | import QrcodeGenerator from 'luna-qrcode-generator.js' 4 | import story from '../share/story' 5 | import readme from './README.md' 6 | 7 | const def = story( 8 | 'qrcode-generator', 9 | (container) => { 10 | const qrcodeGenerator = new QrcodeGenerator(container) 11 | 12 | return qrcodeGenerator 13 | }, 14 | { 15 | readme, 16 | source: __STORY__, 17 | } 18 | ) 19 | 20 | export default def 21 | 22 | export const { qrcodeGenerator } = def 23 | -------------------------------------------------------------------------------- /src/qrcode-generator/style.scss: -------------------------------------------------------------------------------- 1 | @use '../share/mixin' as mixin; 2 | @use '../share/theme' as theme; 3 | 4 | .luna-qrcode-generator { 5 | border: 1px solid theme.$color-border; 6 | display: flex; 7 | height: 250px; 8 | @include mixin.component(); 9 | } 10 | 11 | .controller { 12 | flex-grow: 1; 13 | display: flex; 14 | flex-direction: column; 15 | padding: 10px; 16 | } 17 | 18 | .setting { 19 | flex-grow: 1; 20 | } 21 | 22 | .input { 23 | height: 25%; 24 | textarea { 25 | width: 100%; 26 | height: 100%; 27 | resize: none; 28 | } 29 | } 30 | 31 | .preview { 32 | width: 25%; 33 | .qrcode { 34 | text-align: center; 35 | } 36 | } 37 | 38 | .theme-dark { 39 | border-color: theme.$color-border-dark; 40 | } 41 | -------------------------------------------------------------------------------- /src/retro-emulator/README.md: -------------------------------------------------------------------------------- 1 | # Luna Retro Emulator 2 | 3 | Retro emulator using libretro. 4 | 5 | ## Demo 6 | 7 | https://luna.liriliri.io/?path=/story/retro-emulator 8 | 9 | ## Install 10 | 11 | Add the following script and style to your page. 12 | 13 | ```html 14 | 15 | 16 | ``` 17 | 18 | You can also get it on npm. 19 | 20 | ```bash 21 | npm install luna-retro-emulator --save 22 | ``` 23 | 24 | ```javascript 25 | import 'luna-retro-emulator/luna-retro-emulator.css' 26 | import LunaRetroEmulator from 'luna-retro-emulator' 27 | ``` 28 | 29 | ## Usage 30 | 31 | ```javascript 32 | const retroEmulator = new RetroEmulator(container, { 33 | core: 'https://luna.liriliri.io/fceumm_libretro.js', 34 | browserFS: 'https://luna.liriliri.io/browserfs.min.js', 35 | }) 36 | retroEmulator.load('https://luna.liriliri.io/Contra.nes') 37 | ``` 38 | 39 | ## Configuration 40 | 41 | * browserFS(string): BrowserFS url. 42 | * config(string): RetroArch config. 43 | * controls(boolean): Show controls. 44 | * core(string): Libretro core url. 45 | * coreConfig(string): RetroArch core options. 46 | 47 | ## Api 48 | 49 | ### load(url: string): void 50 | 51 | Load rom from url. 52 | 53 | ### open(): Promise 54 | 55 | Open file and load rom. 56 | 57 | ### pressKey(code: string): void 58 | 59 | Press key. 60 | 61 | ### releaseKey(code: string): void 62 | 63 | Release key. 64 | 65 | ### reset(): void 66 | 67 | Reset game. 68 | 69 | ### toggleFullscreen(): void 70 | 71 | Toggle fullscreen. 72 | -------------------------------------------------------------------------------- /src/retro-emulator/icon.json: -------------------------------------------------------------------------------- 1 | [ 2 | "fullscreen.svg", 3 | "file.svg", 4 | "pause.svg", 5 | "play.svg", 6 | "volume.svg", 7 | "volume-off.svg", 8 | "step-backward.svg", 9 | "step-forward.svg" 10 | ] 11 | -------------------------------------------------------------------------------- /src/retro-emulator/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "retro-emulator", 3 | "version": "0.1.1", 4 | "description": "Retro emulator using libretro", 5 | "luna": { 6 | "icon": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/retro-emulator/story.js: -------------------------------------------------------------------------------- 1 | import 'luna-retro-emulator.css' 2 | import story from '../share/story' 3 | import RetroEmulator from 'luna-retro-emulator.js' 4 | import $ from 'licia/$' 5 | import readme from './README.md' 6 | import { optionsKnob, button, text } from '@storybook/addon-knobs' 7 | 8 | const def = story( 9 | 'retro-emulator', 10 | (container) => { 11 | $(container).css({ 12 | maxWidth: 640, 13 | width: '100%', 14 | margin: '0 auto', 15 | aspectRatio: '1024/768', 16 | }) 17 | 18 | const fcCore = '/fceumm_libretro.js' 19 | 20 | const core = optionsKnob( 21 | 'Core', 22 | { 23 | FC: fcCore, 24 | SFC: '/snes9x_libretro.js', 25 | GBA: '/vba_next_libretro.js', 26 | }, 27 | fcCore, 28 | { 29 | display: 'select', 30 | } 31 | ) 32 | 33 | const config = text('RetroArch Config', 'fps_show = true') 34 | const coreConfig = text( 35 | 'RetroArch Core Options', 36 | core === fcCore ? 'fceumm_turbo_enable = "Player 1"' : '' 37 | ) 38 | 39 | if (core === fcCore) { 40 | const rom = text('ROM', '/Contra.nes') 41 | button('Load', () => { 42 | retroEmulator.load(rom) 43 | return false 44 | }) 45 | } 46 | 47 | const retroEmulator = new RetroEmulator(container, { 48 | core, 49 | coreConfig, 50 | browserFS: '/browserfs.min.js', 51 | config, 52 | }) 53 | 54 | return retroEmulator 55 | }, 56 | { 57 | readme, 58 | source: __STORY__, 59 | } 60 | ) 61 | 62 | export default def 63 | 64 | export const { retroEmulator } = def 65 | -------------------------------------------------------------------------------- /src/retro-emulator/style.scss: -------------------------------------------------------------------------------- 1 | @use '../share/mixin' as mixin; 2 | 3 | .luna-retro-emulator { 4 | width: 100%; 5 | height: 100%; 6 | user-select: none; 7 | position: relative; 8 | min-height: 150px; 9 | min-width: 300px; 10 | @include mixin.component(); 11 | & { 12 | background: #000; 13 | } 14 | } 15 | 16 | @include mixin.controller('retro-emulator'); 17 | 18 | .reset, 19 | .play, 20 | .fast-forward, 21 | .volume { 22 | margin-right: 8px; 23 | height: 100%; 24 | display: inline-block; 25 | vertical-align: top; 26 | } 27 | 28 | .iframe-container { 29 | width: 100%; 30 | height: 100%; 31 | iframe { 32 | width: 100%; 33 | height: 100%; 34 | border: 0; 35 | } 36 | } 37 | 38 | .iframe-mask { 39 | width: 100%; 40 | height: 100%; 41 | position: absolute; 42 | left: 0; 43 | top: 0; 44 | } 45 | -------------------------------------------------------------------------------- /src/retro-emulator/test.js: -------------------------------------------------------------------------------- 1 | import RetroEmulator from './index' 2 | import test from '../share/test' 3 | 4 | test('retro-emulator', (container) => { 5 | const retroEmulator = new RetroEmulator(container, { 6 | core: 'https://luna.liriliri.io/fceumm_libretro.js', 7 | browserFS: 'https://luna.liriliri.io/browserfs.min.js', 8 | }) 9 | 10 | it('basic', function () { 11 | retroEmulator.load('https://luna.liriliri.io/Contra.nes') 12 | }) 13 | 14 | return retroEmulator 15 | }) 16 | -------------------------------------------------------------------------------- /src/retro-handheld/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "retro-handheld", 3 | "version": "0.1.1", 4 | "description": "Retro emulator with controls ui", 5 | "luna": { 6 | "dependencies": [ 7 | "menu", 8 | "retro-emulator" 9 | ] 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/retro-handheld/story.js: -------------------------------------------------------------------------------- 1 | import 'luna-retro-handheld.css' 2 | import $ from 'licia/$' 3 | import story from '../share/story' 4 | import RetroHandheld from 'luna-retro-handheld.js' 5 | import readme from './README.md' 6 | import { optionsKnob, button, text } from '@storybook/addon-knobs' 7 | 8 | const def = story( 9 | 'retro-handheld', 10 | (container) => { 11 | $(container).css({ 12 | maxWidth: 640, 13 | width: '100%', 14 | margin: '0 auto', 15 | }) 16 | 17 | const fcCore = '/fceumm_libretro.js' 18 | 19 | const core = optionsKnob( 20 | 'Core', 21 | { 22 | FC: fcCore, 23 | SFC: '/snes9x_libretro.js', 24 | GBA: '/vba_next_libretro.js', 25 | }, 26 | fcCore, 27 | { 28 | display: 'select', 29 | } 30 | ) 31 | 32 | const config = text('RetroArch Config', 'fps_show = true') 33 | const coreConfig = text( 34 | 'RetroArch Core Options', 35 | core === fcCore ? 'fceumm_turbo_enable = "Player 1"' : '' 36 | ) 37 | 38 | const retroHandheld = new RetroHandheld(container, { 39 | core, 40 | config, 41 | coreConfig, 42 | browserFS: '/browserfs.min.js', 43 | }) 44 | 45 | if (core === fcCore) { 46 | const rom = text('ROM', '/Contra.nes') 47 | button('Load', () => { 48 | retroHandheld.load(rom) 49 | return false 50 | }) 51 | } 52 | 53 | return retroHandheld 54 | }, 55 | { 56 | readme, 57 | story: __STORY__, 58 | } 59 | ) 60 | 61 | export default def 62 | 63 | export const { retroHandheld } = def 64 | -------------------------------------------------------------------------------- /src/retro-handheld/test.js: -------------------------------------------------------------------------------- 1 | import RetroHandheld from './index' 2 | import test from '../share/test' 3 | 4 | test('retro-emulator', (container) => { 5 | const retroHandheld = new RetroHandheld(container, { 6 | core: 'https://luna.liriliri.io/fceumm_libretro.js', 7 | browserFS: 'https://luna.liriliri.io/browserfs.min.js', 8 | }) 9 | 10 | it('basic', function () { 11 | retroHandheld.load('https://luna.liriliri.io/Contra.nes') 12 | }) 13 | 14 | return retroHandheld 15 | }) 16 | -------------------------------------------------------------------------------- /src/scrollbar/README.md: -------------------------------------------------------------------------------- 1 | # Luna Scrollbar 2 | 3 | Custom scrollbar. 4 | 5 | ## Demo 6 | 7 | https://luna.liriliri.io/?path=/story/scrollbar 8 | 9 | ## Install 10 | 11 | Add the following script and style to your page. 12 | 13 | ```html 14 | 15 | 16 | ``` 17 | 18 | You can also get it on npm. 19 | 20 | ```bash 21 | npm install luna-scrollbar --save 22 | ``` 23 | 24 | ```javascript 25 | import 'luna-scrollbar/luna-scrollbar.css' 26 | import LunaScrollbar from 'luna-scrollbar' 27 | ``` 28 | 29 | ## Usage 30 | 31 | ```javascript 32 | const scrollbar = new LunaScrollbar(container) 33 | scrollbar.getContent().innerHTML = 'test' 34 | ``` 35 | 36 | ## Api 37 | 38 | ### getContent(): HTMLElement 39 | 40 | Get content element. 41 | -------------------------------------------------------------------------------- /src/scrollbar/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "scrollbar", 3 | "version": "0.1.0", 4 | "description": "Custom scrollbar" 5 | } 6 | -------------------------------------------------------------------------------- /src/scrollbar/story.js: -------------------------------------------------------------------------------- 1 | import 'luna-scrollbar.css' 2 | import Scrollbar from 'luna-scrollbar.js' 3 | import $ from 'licia/$' 4 | import escape from 'licia/escape' 5 | import story from '../share/story' 6 | import readme from './README.md' 7 | 8 | const def = story( 9 | 'scrollbar', 10 | (container) => { 11 | $(container) 12 | .css({ 13 | maxWidth: 640, 14 | padding: '50px', 15 | width: '100%', 16 | aspectRatio: '4/3', 17 | margin: '0 auto', 18 | border: '1px solid black', 19 | }) 20 | .html( 21 | `
${escape( 22 | readme 23 | )}
` 24 | ) 25 | 26 | const scrollbar = new Scrollbar(container) 27 | 28 | return scrollbar 29 | }, 30 | { 31 | readme, 32 | source: __STORY__, 33 | } 34 | ) 35 | 36 | export default def 37 | 38 | export const { scrollbar } = def 39 | -------------------------------------------------------------------------------- /src/scrollbar/style.scss: -------------------------------------------------------------------------------- 1 | @use '../share/mixin' as mixin; 2 | 3 | .luna-scrollbar { 4 | position: relative; 5 | @include mixin.component(); 6 | &:hover { 7 | .track { 8 | opacity: 1; 9 | } 10 | } 11 | } 12 | 13 | .wrapper { 14 | overflow: hidden; 15 | position: absolute; 16 | left: 0; 17 | right: 0; 18 | top: 0; 19 | bottom: 0; 20 | } 21 | 22 | .offset { 23 | position: absolute; 24 | top: 0; 25 | left: 0; 26 | } 27 | 28 | .content-wrapper { 29 | width: 100%; 30 | height: 100%; 31 | &::-webkit-scrollbar { 32 | display: none; 33 | width: 0; 34 | height: 0; 35 | } 36 | } 37 | 38 | .track { 39 | z-index: 1; 40 | position: absolute; 41 | overflow: hidden; 42 | opacity: 0; 43 | transition: opacity 0.3s; 44 | &.active { 45 | opacity: 1; 46 | } 47 | } 48 | 49 | .thumb { 50 | position: absolute; 51 | touch-action: none; 52 | background: rgba(0, 0, 0, 0.2); 53 | border-radius: 3px; 54 | } 55 | 56 | .horizontal { 57 | left: 2px; 58 | right: 2px; 59 | bottom: 2px; 60 | height: 6px; 61 | .thumb { 62 | top: 0; 63 | bottom: 0; 64 | } 65 | } 66 | 67 | .vertical { 68 | top: 2px; 69 | bottom: 2px; 70 | right: 2px; 71 | width: 6px; 72 | .thumb { 73 | left: 0; 74 | right: 0; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/scrollbar/test.js: -------------------------------------------------------------------------------- 1 | import Scrollbar from './index' 2 | import test from '../share/test' 3 | 4 | test('scrollbar', (container) => { 5 | const scrollbar = new Scrollbar(container) 6 | 7 | it('basic', () => { 8 | expect(scrollbar.getContent().innerHTML).to.equal('') 9 | }) 10 | 11 | return scrollbar 12 | }) 13 | -------------------------------------------------------------------------------- /src/setting/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 2.0.2 (27 Feb 2025) 2 | 3 | * refactor: theme 4 | 5 | ## 2.0.1 (11 Jun 2024) 6 | 7 | * fix: react custom className not working 8 | 9 | ## 2.0.0 (11 Jun 2024) 10 | 11 | * refactor: react implementation 12 | 13 | ## 1.0.1 (20 Apr 2024) 14 | 15 | * fix: react input value 16 | 17 | ## 1.0.0 (18 Dec 2023) 18 | 19 | * chore: rename setting text to input 20 | -------------------------------------------------------------------------------- /src/setting/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "setting", 3 | "version": "2.0.2", 4 | "description": "Settings panel", 5 | "dependencies": { 6 | "micromark": "^3.1.0" 7 | }, 8 | "luna": { 9 | "install": true, 10 | "react": true 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/setting/test.js: -------------------------------------------------------------------------------- 1 | import Setting from './index' 2 | import test from '../share/test' 3 | 4 | test('setting', (container) => { 5 | const setting = new Setting(container) 6 | it('basic', function () { 7 | setting.appendTitle('Test') 8 | }) 9 | }) 10 | -------------------------------------------------------------------------------- /src/setting/util.ts: -------------------------------------------------------------------------------- 1 | export const progress = (val: number, min: number, max: number) => { 2 | return (((val - min) / (max - min)) * 100).toFixed(2) 3 | } 4 | -------------------------------------------------------------------------------- /src/shader-toy-player/README.md: -------------------------------------------------------------------------------- 1 | # Luna Shader Toy Player 2 | 3 | Shader toy player. 4 | 5 | ## Demo 6 | 7 | https://luna.liriliri.io/?path=/story/shader-toy-player 8 | 9 | ## Install 10 | 11 | Add the following script and style to your page. 12 | 13 | ```html 14 | 15 | 16 | ``` 17 | 18 | You can also get it on npm. 19 | 20 | ```bash 21 | npm install luna-shader-toy-player --save 22 | ``` 23 | 24 | ```javascript 25 | import 'luna-shader-toy-player/luna-shader-toy-player.css' 26 | import LunaShaderToyPlayer from 'luna-shader-toy-player' 27 | ``` 28 | 29 | ## Usage 30 | 31 | ```javascript 32 | const container = document.getElementById('container') 33 | const shaderToyPlayer = new LunaShaderToyPlayer(container) 34 | 35 | shaderToyPlayer.setOption('renderPass', [ 36 | { 37 | inputs: [], 38 | outputs: [], 39 | code: `void mainImage( out vec4 fragColor, in vec2 fragCoord ) 40 | { 41 | vec2 uv = fragCoord/iResolution.xy; 42 | vec3 col = 0.5 + 0.5*cos(iTime+uv.xyx+vec3(0,2,4)); 43 | fragColor = vec4(col,1.0); 44 | }`, 45 | name: 'Image', 46 | description: '', 47 | type: 'image', 48 | }, 49 | ]) 50 | ``` 51 | 52 | ## Configuration 53 | 54 | * controls(boolean): Player controls. 55 | * renderPass(any[]): Render pass. 56 | -------------------------------------------------------------------------------- /src/shader-toy-player/icon.json: -------------------------------------------------------------------------------- 1 | [ 2 | "fullscreen.svg", 3 | "pause.svg", 4 | "play.svg", 5 | "volume.svg", 6 | "volume-off.svg", 7 | "step-backward.svg" 8 | ] 9 | -------------------------------------------------------------------------------- /src/shader-toy-player/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shader-toy-player", 3 | "version": "0.5.1", 4 | "description": "Shader toy player", 5 | "luna": { 6 | "icon": true, 7 | "vue": true, 8 | "test": false 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/shader-toy-player/style.scss: -------------------------------------------------------------------------------- 1 | @use '../share/mixin' as mixin; 2 | @use '../share/theme' as theme; 3 | 4 | .luna-shader-toy-player { 5 | width: 100%; 6 | height: 100%; 7 | touch-action: none; 8 | position: relative; 9 | @include mixin.component(); 10 | & { 11 | background: #000; 12 | } 13 | } 14 | 15 | @include mixin.controller('shader-toy-player'); 16 | 17 | .canvas { 18 | width: 100%; 19 | height: 100%; 20 | canvas { 21 | width: 100%; 22 | height: 100%; 23 | } 24 | } 25 | 26 | .reset, 27 | .play, 28 | .volume { 29 | margin-right: 8px; 30 | height: 100%; 31 | display: inline-block; 32 | vertical-align: top; 33 | } 34 | 35 | .time, 36 | .fps-button, 37 | .resolution { 38 | color: #eee; 39 | font-size: #{theme.$font-size-s-m}px; 40 | line-height: 38px; 41 | } 42 | 43 | .fps-button { 44 | cursor: pointer; 45 | } 46 | 47 | .fps { 48 | opacity: 0; 49 | position: absolute; 50 | font-size: #{theme.$font-size-s-m}px; 51 | top: 0; 52 | right: 0; 53 | padding: 4px 8px; 54 | background: rgba(0, 0, 0, 0.5); 55 | color: theme.$color-white; 56 | transition: opacity 0.3s; 57 | &.active { 58 | opacity: 1; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/shader-toy-player/test.js: -------------------------------------------------------------------------------- 1 | import ShaderToyPlayer from './index' 2 | import test from '../share/test' 3 | 4 | const code = `void mainImage( out vec4 fragColor, in vec2 fragCoord ) 5 | { 6 | vec2 uv = fragCoord/iResolution.xy; 7 | vec3 col = 0.5 + 0.5*cos(iTime+uv.xyx+vec3(0,2,4)); 8 | fragColor = vec4(col,1.0); 9 | }` 10 | 11 | test('shader-toy-player', (container) => { 12 | const shaderToyPlayer = new ShaderToyPlayer(container) 13 | 14 | it('basic', function () { 15 | shaderToyPlayer.setOption('renderPass', [ 16 | { 17 | inputs: [], 18 | outputs: [], 19 | code, 20 | name: 'Image', 21 | description: '', 22 | type: 'image', 23 | }, 24 | ]) 25 | }) 26 | 27 | return shaderToyPlayer 28 | }) 29 | -------------------------------------------------------------------------------- /src/shader-toy-player/vue.ts: -------------------------------------------------------------------------------- 1 | import { defineComponent, h, onBeforeUnmount, onMounted, shallowRef } from 'vue' 2 | import ShaderToyPlayer from './index' 3 | 4 | const LunaShaderToyPlayer = defineComponent({ 5 | name: 'LunaShaderToyPlayer', 6 | props: { 7 | style: { 8 | type: Object, 9 | default: () => ({}), 10 | }, 11 | controls: { 12 | type: Boolean, 13 | default: true, 14 | }, 15 | renderPass: { 16 | type: Array, 17 | }, 18 | }, 19 | emits: ['create'], 20 | setup(props, context) { 21 | const container = shallowRef() 22 | const shaderToyPlayer = shallowRef() 23 | 24 | onMounted(() => { 25 | shaderToyPlayer.value = new ShaderToyPlayer(container.value!, { 26 | renderPass: props.renderPass, 27 | controls: props.controls, 28 | }) 29 | 30 | context.emit('create', shaderToyPlayer.value) 31 | }) 32 | 33 | onBeforeUnmount(() => { 34 | shaderToyPlayer.value?.destroy() 35 | }) 36 | 37 | return () => { 38 | return h('div', { 39 | ref: container, 40 | style: props.style, 41 | }) 42 | } 43 | }, 44 | }) 45 | 46 | export default LunaShaderToyPlayer 47 | -------------------------------------------------------------------------------- /src/share/icon/add.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /src/share/icon/arrow-left.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | -------------------------------------------------------------------------------- /src/share/icon/arrow-right.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | -------------------------------------------------------------------------------- /src/share/icon/bold.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /src/share/icon/brush.svg: -------------------------------------------------------------------------------- 1 | 3 | 6 | -------------------------------------------------------------------------------- /src/share/icon/camera.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/share/icon/caret-down.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /src/share/icon/caret-right.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /src/share/icon/caret-up.svg: -------------------------------------------------------------------------------- 1 | 2 | 13 | 17 | 18 | -------------------------------------------------------------------------------- /src/share/icon/check.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/share/icon/close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/share/icon/copy.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/share/icon/crosshair.svg: -------------------------------------------------------------------------------- 1 | 3 | 6 | -------------------------------------------------------------------------------- /src/share/icon/delete.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/share/icon/download.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/share/icon/eraser.svg: -------------------------------------------------------------------------------- 1 | 3 | 6 | -------------------------------------------------------------------------------- /src/share/icon/error.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/share/icon/eye.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/share/icon/eyedropper.svg: -------------------------------------------------------------------------------- 1 | 3 | 6 | -------------------------------------------------------------------------------- /src/share/icon/fullscreen.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/share/icon/hand.svg: -------------------------------------------------------------------------------- 1 | 3 | 6 | -------------------------------------------------------------------------------- /src/share/icon/header.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /src/share/icon/horizontal-rule.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /src/share/icon/info.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/share/icon/input.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /src/share/icon/italic.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /src/share/icon/list.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /src/share/icon/loop-all.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /src/share/icon/loop-off.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /src/share/icon/loop-one.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /src/share/icon/maximize.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/share/icon/maximized.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/share/icon/minimize.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/share/icon/output.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/share/icon/paint-bucket.svg: -------------------------------------------------------------------------------- 1 | 3 | 6 | -------------------------------------------------------------------------------- /src/share/icon/pause.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /src/share/icon/pencil.svg: -------------------------------------------------------------------------------- 1 | 3 | 6 | -------------------------------------------------------------------------------- /src/share/icon/pip.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /src/share/icon/play.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /src/share/icon/quote.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /src/share/icon/reset-color.svg: -------------------------------------------------------------------------------- 1 | 2 | 15 | 20 | -------------------------------------------------------------------------------- /src/share/icon/shuffle-disabled.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /src/share/icon/shuffle.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /src/share/icon/step-backward.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/share/icon/step-forward.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/share/icon/strike-through.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /src/share/icon/swap.svg: -------------------------------------------------------------------------------- 1 | 2 | 14 | 21 | 22 | -------------------------------------------------------------------------------- /src/share/icon/underline.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /src/share/icon/volume-down.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /src/share/icon/volume-off.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /src/share/icon/volume.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /src/share/icon/warn.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/share/icon/zoom-in.svg: -------------------------------------------------------------------------------- 1 | 4 | 7 | 10 | -------------------------------------------------------------------------------- /src/share/icon/zoom-out.svg: -------------------------------------------------------------------------------- 1 | 4 | 7 | 10 | -------------------------------------------------------------------------------- /src/share/icon/zoom.svg: -------------------------------------------------------------------------------- 1 | 3 | 6 | -------------------------------------------------------------------------------- /src/share/react.tsx: -------------------------------------------------------------------------------- 1 | import { FC, PropsWithChildren } from 'react' 2 | import { classPrefix, getPlatform } from './util' 3 | import className from 'licia/className' 4 | 5 | interface IComponentProps { 6 | compName: string 7 | theme?: string 8 | className?: string 9 | } 10 | 11 | export const Component: FC> = (props) => { 12 | const c = classPrefix(props.compName) 13 | 14 | return ( 15 |
23 | {props.children} 24 |
25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /src/share/test.js: -------------------------------------------------------------------------------- 1 | import toArr from 'licia/toArr' 2 | import each from 'licia/each' 3 | import contain from 'licia/contain' 4 | 5 | /* eslint-disable no-undef */ 6 | const karma = __karma__ 7 | 8 | export default function (name, testFn) { 9 | const isHeadless = karma.config.headless 10 | if (!isHeadless && window.location.pathname === '/context.html') { 11 | window.open('/debug.html', 'debugTab') 12 | } 13 | describe(name, function () { 14 | const container = document.createElement('div') 15 | document.body.appendChild(container) 16 | let components = testFn(container) 17 | if (components) { 18 | components = toArr(components) 19 | window.components = components 20 | window.component = components[0] 21 | } 22 | if (isHeadless) { 23 | after(function () { 24 | if (window.components) { 25 | each(window.components, (component) => component.destroy()) 26 | } 27 | }) 28 | } 29 | }) 30 | } 31 | 32 | export function getPublicPath(p) { 33 | let isMatch = false 34 | each(karma.files, (val, file) => { 35 | if (isMatch) { 36 | return 37 | } 38 | if (contain(file, `public/${p}`)) { 39 | p = file 40 | isMatch = true 41 | } 42 | }) 43 | return p 44 | } 45 | -------------------------------------------------------------------------------- /src/share/theme.json: -------------------------------------------------------------------------------- 1 | { 2 | "token": { 3 | "colorPrimary": "#1a73e8", 4 | "fontFamilyCode": "ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/share/types.ts: -------------------------------------------------------------------------------- 1 | // https://stackoverflow.com/questions/57835286/deep-recursive-requiredt-on-specific-properties 2 | export type DeepRequired = T extends object 3 | ? Omit> & 4 | Required<{ 5 | [K in Extract]: NonNullable< 6 | DeepRequired> 7 | > 8 | }> 9 | : T 10 | 11 | type Shift = ((...t: T) => any) extends ( 12 | first: any, 13 | ...rest: infer Rest 14 | ) => any 15 | ? Rest 16 | : never 17 | 18 | type ShiftUnion = T extends any[] ? Shift : never 19 | -------------------------------------------------------------------------------- /src/syntax-highlighter/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.0.0 (6 Nov 2022) 2 | 3 | * fix: darkmode scrollbar color-scheme -------------------------------------------------------------------------------- /src/syntax-highlighter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "syntax-highlighter", 3 | "version": "1.0.0", 4 | "description": "Syntax highlighter using highlightjs", 5 | "dependencies": { 6 | "highlight.js": "^11.4.0" 7 | }, 8 | "luna": { 9 | "install": true, 10 | "dependencies": [ 11 | "text-viewer" 12 | ] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/syntax-highlighter/story.js: -------------------------------------------------------------------------------- 1 | import 'luna-syntax-highlighter.css' 2 | import SyntaxHighlighter from 'luna-syntax-highlighter.js' 3 | import readme from './README.md' 4 | import changelog from './CHANGELOG.md' 5 | import story from '../share/story' 6 | import { text, boolean, optionsKnob, number } from '@storybook/addon-knobs' 7 | import componentCode from '!!raw-loader!./index' 8 | 9 | const def = story( 10 | 'syntax-highlighter', 11 | (container) => { 12 | const code = text('Code', componentCode) 13 | const language = optionsKnob( 14 | 'Language', 15 | { 16 | 'HTML, XML': 'xml', 17 | CSS: 'css', 18 | JavaScript: 'javascript', 19 | }, 20 | 'javascript', 21 | { display: 'select' } 22 | ) 23 | const showLineNumbers = boolean('Show Line Numbers', true) 24 | const wrapLongLines = boolean('Wrap Long Lines', true) 25 | const maxHeight = number('Max Height', 400, { 26 | range: true, 27 | min: 50, 28 | max: 2000, 29 | }) 30 | 31 | const syntaxHighlighter = new SyntaxHighlighter(container, { 32 | code, 33 | language, 34 | showLineNumbers, 35 | wrapLongLines, 36 | maxHeight, 37 | }) 38 | 39 | return syntaxHighlighter 40 | }, 41 | { 42 | readme, 43 | changelog, 44 | source: __STORY__, 45 | themes: { 46 | 'Vs Dark': 'vs-dark', 47 | }, 48 | } 49 | ) 50 | 51 | export default def 52 | 53 | export const { syntaxHighlighter } = def 54 | -------------------------------------------------------------------------------- /src/syntax-highlighter/test.js: -------------------------------------------------------------------------------- 1 | import SyntaxHighlighter from './index' 2 | import test from '../share/test' 3 | 4 | test('syntax-highlighter', (container) => { 5 | const syntaxHighlighter = new SyntaxHighlighter(container) 6 | 7 | it('basic', () => { 8 | syntaxHighlighter.setOption({ 9 | code: 'const a = 1;', 10 | language: 'javascript', 11 | }) 12 | }) 13 | 14 | return syntaxHighlighter 15 | }) 16 | -------------------------------------------------------------------------------- /src/tab/README.md: -------------------------------------------------------------------------------- 1 | # Luna Tab 2 | 3 | Easy tabs. 4 | 5 | ## Demo 6 | 7 | https://luna.liriliri.io/?path=/story/tab 8 | 9 | ## Install 10 | 11 | Add the following script and style to your page. 12 | 13 | ```html 14 | 15 | 16 | ``` 17 | 18 | You can also get it on npm. 19 | 20 | ```bash 21 | npm install luna-tab --save 22 | ``` 23 | 24 | ```javascript 25 | import 'luna-tab/luna-tab.css' 26 | import LunaTab from 'luna-tab' 27 | ``` 28 | 29 | ## Usage 30 | 31 | ```javascript 32 | const container = document.getElementById('container') 33 | const tab = new LunaTabs(container, { 34 | height: 30, 35 | }) 36 | tab.append({ 37 | id: 'console', 38 | title: 'Console', 39 | }) 40 | tab.select('console') 41 | tab.on('select', id => { 42 | console.log(id) 43 | }) 44 | ``` 45 | 46 | ## Configuration 47 | 48 | * height(number): Tab height. 49 | 50 | ## Api 51 | 52 | ### append(tab: ITab): void 53 | 54 | Append tab. 55 | 56 | ### deselect(): void 57 | 58 | Deselect tabs. 59 | 60 | ### insert(pos: number, tab: ITab): void 61 | 62 | Insert tab at given position. 63 | 64 | ### remove(id: string): void 65 | 66 | Remove tab. 67 | 68 | ### select(id: string): void 69 | 70 | Select tab. 71 | 72 | ## Types 73 | 74 | ### ITab 75 | 76 | * closeable(boolean): Whether tab is closeable. 77 | * id(string): Tab id. 78 | * title(string): Tab title. 79 | -------------------------------------------------------------------------------- /src/tab/icon.json: -------------------------------------------------------------------------------- 1 | ["close.svg"] 2 | -------------------------------------------------------------------------------- /src/tab/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tab", 3 | "version": "0.4.3", 4 | "description": "Easy tabs", 5 | "luna": { 6 | "react": true, 7 | "icon": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/tab/test.js: -------------------------------------------------------------------------------- 1 | import Tab from './index' 2 | import test from '../share/test' 3 | 4 | test('tab', (container) => { 5 | const tab = new Tab(container) 6 | 7 | it('basic', function () { 8 | tab.append({ 9 | id: 'console', 10 | title: 'Console', 11 | }) 12 | const $item = $(container).find(tab.c('.item')) 13 | expect($item.text()).to.equal('Console') 14 | }) 15 | 16 | return tab 17 | }) 18 | -------------------------------------------------------------------------------- /src/tag-input/README.md: -------------------------------------------------------------------------------- 1 | # Luna Tag Input 2 | 3 | Lightweight tags input. 4 | 5 | ## Demo 6 | 7 | https://luna.liriliri.io/?path=/story/tag-input 8 | 9 | ## Install 10 | 11 | Add the following script and style to your page. 12 | 13 | ```html 14 | 15 | 16 | ``` 17 | 18 | You can also get it on npm. 19 | 20 | ```bash 21 | npm install luna-tag-input --save 22 | ``` 23 | 24 | ```javascript 25 | import 'luna-tag-input/luna-tag-input.css' 26 | import LunaTagInput from 'luna-tag-input' 27 | ``` 28 | -------------------------------------------------------------------------------- /src/tag-input/index.ts: -------------------------------------------------------------------------------- 1 | import { exportCjs } from '../share/util' 2 | import Component from '../share/Component' 3 | 4 | /** 5 | * Lightweight tags input. 6 | */ 7 | export default class TagInput extends Component { 8 | constructor(container: HTMLInputElement) { 9 | super(container, { compName: 'tag-input' }) 10 | } 11 | } 12 | 13 | if (typeof module !== 'undefined') { 14 | exportCjs(module, TagInput) 15 | } 16 | -------------------------------------------------------------------------------- /src/tag-input/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tag-input", 3 | "version": "0.1.0", 4 | "description": "Lightweight tags input", 5 | "luna": { 6 | "test": false 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/tag-input/story.js: -------------------------------------------------------------------------------- 1 | import 'luna-tag-input.css' 2 | import h from 'licia/h' 3 | import TagInput from 'luna-tag-input.js' 4 | import story from '../share/story' 5 | import readme from './README.md' 6 | 7 | const def = story( 8 | 'tag-input', 9 | (wrapper) => { 10 | const container = h('input') 11 | wrapper.appendChild(container) 12 | const tagInput = new TagInput(container) 13 | 14 | return tagInput 15 | }, 16 | { 17 | readme, 18 | source: __STORY__, 19 | } 20 | ) 21 | 22 | export default def 23 | 24 | export const { tagInput } = def 25 | -------------------------------------------------------------------------------- /src/tag-input/style.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liriliri/luna/09cfb448f0e8646fb790feb6ca03e6799e5dc697/src/tag-input/style.scss -------------------------------------------------------------------------------- /src/text-viewer/README.md: -------------------------------------------------------------------------------- 1 | # Luna Text Viewer 2 | 3 | Text viewer with line number. 4 | 5 | ## Demo 6 | 7 | https://luna.liriliri.io/?path=/story/text-viewer 8 | 9 | ## Install 10 | 11 | Add the following script and style to your page. 12 | 13 | ```html 14 | 15 | 16 | ``` 17 | 18 | You can also get it on npm. 19 | 20 | ```bash 21 | npm install luna-text-viewer --save 22 | ``` 23 | 24 | ```javascript 25 | import 'luna-text-viewer/luna-text-viewer.css' 26 | import LunaTextViewer from 'luna-text-viewer' 27 | ``` 28 | 29 | ## Usage 30 | 31 | ```javascript 32 | const textViewer = new LunaTextViewer(container) 33 | textViewer.setOption({ 34 | text: 'Luna Text Viewer', 35 | }) 36 | ``` 37 | 38 | ## Configuration 39 | 40 | * escape(boolean): Whether to escape text or not. 41 | * maxHeight(number): Max viewer height. 42 | * showLineNumbers(boolean): Show line numbers. 43 | * text(string): Text to view. 44 | * wrapLongLines(boolean): Wrap lone lines. 45 | 46 | ## Api 47 | 48 | ### append(text: string): undefined | $ 49 | 50 | Append text. 51 | -------------------------------------------------------------------------------- /src/text-viewer/icon.json: -------------------------------------------------------------------------------- 1 | ["check.svg", "copy.svg"] 2 | -------------------------------------------------------------------------------- /src/text-viewer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "text-viewer", 3 | "version": "0.2.1", 4 | "description": "Text viewer with line number", 5 | "luna": { 6 | "icon": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/text-viewer/story.js: -------------------------------------------------------------------------------- 1 | import 'luna-text-viewer.css' 2 | import TextViewer from 'luna-text-viewer.js' 3 | import readme from './README.md' 4 | import story from '../share/story' 5 | import { text, boolean, number, button } from '@storybook/addon-knobs' 6 | 7 | const def = story( 8 | 'text-viewer', 9 | (container) => { 10 | const txt = text('Text', readme) 11 | const showLineNumbers = boolean('Show Line Numbers', true) 12 | const wrapLongLines = boolean('Wrap Long Lines', true) 13 | 14 | const maxHeight = number('Max Height', 400, { 15 | range: true, 16 | min: 50, 17 | max: 1000, 18 | }) 19 | 20 | const txtToAppend = text( 21 | 'Text to Append', 22 | '\n# Luna Text Viewer\n\nText viewer with line number.\n' 23 | ) 24 | button('Append', () => { 25 | textViewer.append(txtToAppend) 26 | return false 27 | }) 28 | 29 | const textViewer = new TextViewer(container, { 30 | text: txt, 31 | showLineNumbers, 32 | wrapLongLines, 33 | maxHeight, 34 | }) 35 | 36 | return textViewer 37 | }, 38 | { 39 | readme, 40 | source: __STORY__, 41 | } 42 | ) 43 | 44 | export default def 45 | 46 | export const { textViewer } = def 47 | -------------------------------------------------------------------------------- /src/text-viewer/test.js: -------------------------------------------------------------------------------- 1 | import TextViewer from './index' 2 | import test from '../share/test' 3 | 4 | test('text-viewer', (container) => { 5 | const textViewer = new TextViewer(container) 6 | 7 | it('basic', () => { 8 | textViewer.setOption({ 9 | code: 'const a = 1;', 10 | }) 11 | }) 12 | 13 | return textViewer 14 | }) 15 | -------------------------------------------------------------------------------- /src/toolbar/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "toolbar", 3 | "version": "0.9.2", 4 | "description": "Application toolbar", 5 | "luna": { 6 | "react": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/toolbar/test.js: -------------------------------------------------------------------------------- 1 | import Toolbar from './index' 2 | import test from '../share/test' 3 | 4 | test('toolbar', (container) => { 5 | const toolbar = new Toolbar(container) 6 | it('basic', function () { 7 | toolbar.appendText('Test') 8 | }) 9 | }) 10 | -------------------------------------------------------------------------------- /src/video-player/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.0.0 (25 Dec 2023) 2 | 3 | * feat: hotkey 4 | -------------------------------------------------------------------------------- /src/video-player/README.md: -------------------------------------------------------------------------------- 1 | # Luna Video Player 2 | 3 | Elegant HTML5 video player. 4 | 5 | ## Demo 6 | 7 | https://luna.liriliri.io/?path=/story/video-player 8 | 9 | ## Install 10 | 11 | Add the following script and style to your page. 12 | 13 | ```html 14 | 15 | 16 | ``` 17 | 18 | You can also get it on npm. 19 | 20 | ```bash 21 | npm install luna-video-player --save 22 | ``` 23 | 24 | ```javascript 25 | import 'luna-video-player/luna-video-player.css' 26 | import LunaVideoPlayer from 'luna-video-player' 27 | ``` 28 | 29 | ## Usage 30 | 31 | ```javascript 32 | const container = document.getElementById('container') 33 | const videoPlayer = new LunaVideoPlayer(container, { 34 | url: 'https://api.dogecloud.com/player/get.mp4?vcode=9dbb405e2141b5e8&userId=2096&flsign=1c02d5e60d2a0f29e1fd2ec0c0762b8b&ext=.mp4', 35 | }) 36 | 37 | videoPlayer.play() 38 | ``` 39 | 40 | ## Configuration 41 | 42 | * hotkey(boolean): Enable hotkey. 43 | * url(string): Video url. 44 | 45 | ## Api 46 | 47 | ### pause(): void 48 | 49 | Pause video. 50 | 51 | ### play(): undefined | Promise 52 | 53 | Play video. 54 | 55 | ### seek(time: number): void 56 | 57 | Seek to specified time. 58 | 59 | ### volume(percentage: number): void 60 | 61 | Set video volume. 62 | -------------------------------------------------------------------------------- /src/video-player/icon.json: -------------------------------------------------------------------------------- 1 | [ 2 | "fullscreen.svg", 3 | "pause.svg", 4 | "play.svg", 5 | "volume-off.svg", 6 | "volume.svg", 7 | "camera.svg", 8 | "pip.svg", 9 | "volume-down.svg" 10 | ] 11 | -------------------------------------------------------------------------------- /src/video-player/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "video-player", 3 | "version": "1.0.0", 4 | "description": "Video player", 5 | "luna": { 6 | "icon": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/video-player/story.js: -------------------------------------------------------------------------------- 1 | import 'luna-video-player.css' 2 | import story from '../share/story' 3 | import VideoPlayer from 'luna-video-player.js' 4 | import $ from 'licia/$' 5 | import readme from './README.md' 6 | import changelog from './CHANGELOG.md' 7 | import { text } from '@storybook/addon-knobs' 8 | 9 | const def = story( 10 | 'video-player', 11 | (container) => { 12 | $(container).css({ 13 | maxWidth: 640, 14 | width: '100%', 15 | margin: '0 auto', 16 | minHeight: 150, 17 | aspectRatio: '1280/720', 18 | }) 19 | 20 | const url = text( 21 | 'Video Url', 22 | 'https://api.dogecloud.com/player/get.mp4?vcode=9dbb405e2141b5e8&userId=2096&flsign=1c02d5e60d2a0f29e1fd2ec0c0762b8b&ext=.mp4' 23 | ) 24 | 25 | const videoPlayer = new VideoPlayer(container, { 26 | url, 27 | }) 28 | 29 | return videoPlayer 30 | }, 31 | { 32 | readme, 33 | changelog, 34 | source: __STORY__, 35 | } 36 | ) 37 | 38 | export default def 39 | 40 | export const { videoPlayer } = def 41 | -------------------------------------------------------------------------------- /src/video-player/test.js: -------------------------------------------------------------------------------- 1 | import VideoPlayer from './index' 2 | import test from '../share/test' 3 | 4 | test('video-player', (container) => { 5 | it('basic', () => { 6 | const videoPlayer = new VideoPlayer(container) 7 | videoPlayer.play() 8 | }) 9 | }) 10 | -------------------------------------------------------------------------------- /src/virtual-list/README.md: -------------------------------------------------------------------------------- 1 | # Luna Virtual List 2 | 3 | Vertical list with virtual scrolling. 4 | 5 | ## Demo 6 | 7 | https://luna.liriliri.io/?path=/story/virtual-list 8 | 9 | ## Install 10 | 11 | Add the following script and style to your page. 12 | 13 | ```html 14 | 15 | 16 | ``` 17 | 18 | You can also get it on npm. 19 | 20 | ```bash 21 | npm install luna-virtual-list --save 22 | ``` 23 | 24 | ```javascript 25 | import 'luna-virtual-list/luna-virtual-list.css' 26 | import LunaVirtualList from 'luna-virtual-list' 27 | ``` 28 | 29 | ## Usage 30 | 31 | ```javascript 32 | const virtualList = new VirtualList(container, { 33 | autoScroll: true, 34 | }) 35 | virtualList.append(document.createElement('div')) 36 | ``` 37 | 38 | ## Configuration 39 | 40 | * autoScroll(boolean): Auto scroll if at bottom. 41 | 42 | ## Api 43 | 44 | ### append(el: HTMLElement): void 45 | 46 | Append item. 47 | 48 | ### clear(): void 49 | 50 | Clear all items. 51 | 52 | ### remove(el: HTMLElement): void 53 | 54 | Remove item. 55 | 56 | ### scrollToEnd(): void 57 | 58 | Scroll to end. 59 | 60 | ### setItems(els: HTMLElement[]): void 61 | 62 | Set items. 63 | 64 | ### update(el?: HTMLElement): void 65 | 66 | Update heights. 67 | -------------------------------------------------------------------------------- /src/virtual-list/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "virtual-list", 3 | "version": "0.1.1", 4 | "description": "Vertical list with virtual scrolling" 5 | } 6 | -------------------------------------------------------------------------------- /src/virtual-list/style.scss: -------------------------------------------------------------------------------- 1 | @use '../share/mixin' as mixin; 2 | 3 | .luna-virtual-list { 4 | @include mixin.overflow-auto(); 5 | height: 100%; 6 | position: relative; 7 | will-change: scroll-position; 8 | transform: translate3d(0, 0, 0); 9 | } 10 | 11 | .fake-items { 12 | position: absolute; 13 | top: 0; 14 | left: 0; 15 | pointer-events: none; 16 | visibility: hidden; 17 | } 18 | 19 | .items { 20 | position: absolute; 21 | top: 0; 22 | left: 0; 23 | } 24 | -------------------------------------------------------------------------------- /src/virtual-list/test.js: -------------------------------------------------------------------------------- 1 | import VirtualList from './index' 2 | import test from '../share/test' 3 | 4 | test('virtual-list', (container) => { 5 | const virtualList = new VirtualList(container) 6 | 7 | it('basic', () => { 8 | virtualList.append(document.createElement('div')) 9 | }) 10 | 11 | return virtualList 12 | }) 13 | -------------------------------------------------------------------------------- /src/window/README.md: -------------------------------------------------------------------------------- 1 | # Luna Window 2 | 3 | HTML5 window manager. 4 | 5 | ## Demo 6 | 7 | https://luna.liriliri.io/?path=/story/window 8 | 9 | ## Install 10 | 11 | Add the following script and style to your page. 12 | 13 | ```html 14 | 15 | 16 | ``` 17 | 18 | You can also get it on npm. 19 | 20 | ```bash 21 | npm install luna-window --save 22 | ``` 23 | 24 | ```javascript 25 | import 'luna-window/luna-window.css' 26 | import LunaWindow from 'luna-window' 27 | ``` 28 | 29 | ## Usage 30 | 31 | ```javascript 32 | const win = new LunaWindow({ 33 | title: 'Window Title', 34 | x: 50, 35 | y: 50, 36 | width: 800, 37 | height: 600, 38 | content: 'This is the content.' 39 | }) 40 | win.show() 41 | ``` 42 | 43 | ## Configuration 44 | 45 | * content(string|HTMLElement): Content to display, url is supported. 46 | * height(number): Height of the window. 47 | * minHeight(number): Minimum height of the window. 48 | * minWidth(number): Minimum width of the window. 49 | * title(string): Title of the window. 50 | * width(number): Width of the window. 51 | * x(number): Offset to the left of the viewport. 52 | * y(number): Offset to the top of the viewport. 53 | 54 | ## Api 55 | 56 | ### maximize(): void 57 | 58 | Maximize the window. 59 | 60 | ### minimize(): void 61 | 62 | Minimize the window. 63 | 64 | ### show(): void 65 | 66 | Show the window. 67 | -------------------------------------------------------------------------------- /src/window/icon.json: -------------------------------------------------------------------------------- 1 | ["close.svg", "maximize.svg", "maximized.svg", "minimize.svg"] 2 | -------------------------------------------------------------------------------- /src/window/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "window", 3 | "version": "0.1.1", 4 | "description": "HTML5 window manager", 5 | "luna": { 6 | "icon": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/window/test.js: -------------------------------------------------------------------------------- 1 | import Window from './index' 2 | import test from '../share/test' 3 | 4 | test('window', () => { 5 | const win = new Window({ 6 | title: 'Window Title', 7 | x: 50, 8 | y: 50, 9 | width: 800, 10 | height: 600, 11 | content: 'This is the content.', 12 | }) 13 | 14 | it('basic', function () { 15 | win.show() 16 | }) 17 | 18 | return win 19 | }) 20 | --------------------------------------------------------------------------------