├── .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 |
--------------------------------------------------------------------------------
/src/file-list/asset/file.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/file-list/asset/folder.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/file-list/asset/image.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/file-list/asset/text.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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 |
32 |
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 |
--------------------------------------------------------------------------------
/src/share/icon/arrow-left.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/share/icon/arrow-right.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/share/icon/bold.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/share/icon/brush.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/share/icon/camera.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/share/icon/caret-down.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/share/icon/caret-right.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/share/icon/caret-up.svg:
--------------------------------------------------------------------------------
1 |
2 |
18 |
--------------------------------------------------------------------------------
/src/share/icon/check.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/share/icon/close.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/share/icon/copy.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/share/icon/crosshair.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/share/icon/delete.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/share/icon/download.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/share/icon/eraser.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/share/icon/error.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/share/icon/eye.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/share/icon/eyedropper.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/share/icon/fullscreen.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/share/icon/hand.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/share/icon/header.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/share/icon/horizontal-rule.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/share/icon/info.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/share/icon/input.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/share/icon/italic.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/share/icon/list.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/share/icon/loop-all.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/share/icon/loop-off.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/share/icon/loop-one.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/share/icon/maximize.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/share/icon/maximized.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/share/icon/minimize.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/share/icon/output.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/share/icon/paint-bucket.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/share/icon/pause.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/share/icon/pencil.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/share/icon/pip.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/share/icon/play.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/share/icon/quote.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/share/icon/reset-color.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/share/icon/shuffle-disabled.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/share/icon/shuffle.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/share/icon/step-backward.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/share/icon/step-forward.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/share/icon/strike-through.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/share/icon/swap.svg:
--------------------------------------------------------------------------------
1 |
2 |
22 |
--------------------------------------------------------------------------------
/src/share/icon/underline.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/share/icon/volume-down.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/share/icon/volume-off.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/share/icon/volume.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/share/icon/warn.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/share/icon/zoom-in.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/share/icon/zoom-out.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/share/icon/zoom.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------