├── src ├── index.ts ├── root.tsx ├── entry.ssr.tsx ├── entry.dev.tsx └── components │ └── wrap-balancer.tsx ├── bun.lockb ├── .prettierignore ├── .eslintignore ├── vite.config.ts ├── .gitignore ├── tsconfig.json ├── README.md ├── .eslintrc.cjs └── package.json /src/index.ts: -------------------------------------------------------------------------------- 1 | export { WrapBalancer } from "./components/wrap-balancer"; 2 | -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harshmangalam/qwik-wrap-balancer/HEAD/bun.lockb -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Files Prettier should not format 2 | **/*.log 3 | **/.DS_Store 4 | *. 5 | dist 6 | node_modules 7 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | **/*.log 2 | **/.DS_Store 3 | *. 4 | .vscode/settings.json 5 | .history 6 | .yarn 7 | bazel-* 8 | bazel-bin 9 | bazel-out 10 | bazel-qwik 11 | bazel-testlogs 12 | dist 13 | dist-dev 14 | lib 15 | lib-types 16 | etc 17 | external 18 | node_modules 19 | temp 20 | tsc-out 21 | tsdoc-metadata.json 22 | target 23 | output 24 | rollup.config.js 25 | build 26 | .cache 27 | .vscode 28 | .rollup.cache 29 | dist 30 | tsconfig.tsbuildinfo 31 | vite.config.ts 32 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import { qwikVite } from '@builder.io/qwik/optimizer'; 3 | 4 | export default defineConfig(() => { 5 | return { 6 | build: { 7 | target: 'es2020', 8 | lib: { 9 | entry: './src/index.ts', 10 | formats: ['es', 'cjs'], 11 | fileName: (format) => `index.qwik.${format === 'es' ? 'mjs' : 'cjs'}`, 12 | }, 13 | }, 14 | plugins: [qwikVite()], 15 | }; 16 | }); 17 | -------------------------------------------------------------------------------- /src/root.tsx: -------------------------------------------------------------------------------- 1 | import { WrapBalancer } from "./components/wrap-balancer"; 2 | 3 | export default () => { 4 | return ( 5 | <> 6 | 7 | 8 | Qwik Wrap Balancer 9 | 10 | 11 |

Qwik: A JavaScript framework reimagined for the edge.

12 | 13 |

Qwik: A JavaScript framework reimagined for the edge.

14 |
15 | 16 | 17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Build 2 | /dist 3 | /lib 4 | /lib-types 5 | /server 6 | 7 | # Development 8 | node_modules 9 | 10 | # Cache 11 | .cache 12 | .mf 13 | .vscode 14 | .rollup.cache 15 | tsconfig.tsbuildinfo 16 | 17 | # Logs 18 | logs 19 | *.log 20 | npm-debug.log* 21 | yarn-debug.log* 22 | yarn-error.log* 23 | pnpm-debug.log* 24 | lerna-debug.log* 25 | 26 | # Editor 27 | !.vscode/extensions.json 28 | .idea 29 | .DS_Store 30 | *.suo 31 | *.ntvs* 32 | *.njsproj 33 | *.sln 34 | *.sw? 35 | 36 | # Yarn 37 | .yarn/* 38 | !.yarn/releases 39 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "target": "ES2017", 5 | "module": "ES2020", 6 | "lib": ["es2020", "DOM"], 7 | "jsx": "react-jsx", 8 | "jsxImportSource": "@builder.io/qwik", 9 | "strict": true, 10 | "declaration": true, 11 | "declarationDir": "lib-types", 12 | "resolveJsonModule": true, 13 | "moduleResolution": "node", 14 | "esModuleInterop": true, 15 | "skipLibCheck": true, 16 | "incremental": true, 17 | "isolatedModules": true, 18 | "types": ["vite/client"] 19 | }, 20 | "include": ["src"] 21 | } 22 | -------------------------------------------------------------------------------- /src/entry.ssr.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * WHAT IS THIS FILE? 3 | * 4 | * SSR entry point, in all cases the application is render outside the browser, this 5 | * entry point will be the common one. 6 | * 7 | * - Server (express, cloudflare...) 8 | * - npm run start 9 | * - npm run preview 10 | * - npm run build 11 | * 12 | */ 13 | import { renderToStream, RenderToStreamOptions } from '@builder.io/qwik/server'; 14 | import { manifest } from '@qwik-client-manifest'; 15 | import Root from './root'; 16 | 17 | export default function (opts: RenderToStreamOptions) { 18 | return renderToStream(, { 19 | manifest, 20 | ...opts, 21 | }); 22 | } 23 | -------------------------------------------------------------------------------- /src/entry.dev.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * WHAT IS THIS FILE? 3 | * 4 | * Development entry point using only client-side modules: 5 | * - Do not use this mode in production! 6 | * - No SSR 7 | * - No portion of the application is pre-rendered on the server. 8 | * - All of the application is running eagerly in the browser. 9 | * - More code is transferred to the browser than in SSR mode. 10 | * - Optimizer/Serialization/Deserialization code is not exercised! 11 | */ 12 | import { render, RenderOptions } from '@builder.io/qwik'; 13 | import Root from './root'; 14 | 15 | export default function (opts: RenderOptions) { 16 | return render(document, , opts); 17 | } 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Qwik Wrap Balancer ⚡️ 2 | 3 | --- 4 | 5 | ## Install 6 | 7 | ```sh 8 | npm i qwikjs-wrap-balancer 9 | ``` 10 | 11 | ```sh 12 | pnpm i qwikjs-wrap-balancer 13 | ``` 14 | 15 | ```sh 16 | yarn add qwikjs-wrap-balancer 17 | ``` 18 | 19 | ```sh 20 | bun install qwikjs-wrap-balancer 21 | ``` 22 | 23 | ## Example 24 | 25 | ```js 26 | import { WrapBalancer } from "qwikjs-wrap-balancer"; 27 | import { component$ } from "@builder.io/qwik"; 28 | export default component$(() => { 29 | return ( 30 | 31 |

Build instantly-interactive web apps without effort.

32 |
33 | ); 34 | }); 35 | ``` 36 | 37 | Screenshot 38 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | browser: true, 5 | es2021: true, 6 | node: true, 7 | }, 8 | extends: [ 9 | 'eslint:recommended', 10 | 'plugin:@typescript-eslint/recommended', 11 | 'plugin:qwik/recommended', 12 | ], 13 | parser: '@typescript-eslint/parser', 14 | parserOptions: { 15 | tsconfigRootDir: __dirname, 16 | project: ['./tsconfig.json'], 17 | ecmaVersion: 2021, 18 | sourceType: 'module', 19 | ecmaFeatures: { 20 | jsx: true, 21 | }, 22 | }, 23 | plugins: ['@typescript-eslint'], 24 | rules: { 25 | '@typescript-eslint/no-explicit-any': 'off', 26 | '@typescript-eslint/explicit-module-boundary-types': 'off', 27 | '@typescript-eslint/no-inferrable-types': 'off', 28 | '@typescript-eslint/no-non-null-assertion': 'off', 29 | '@typescript-eslint/no-empty-interface': 'off', 30 | '@typescript-eslint/no-namespace': 'off', 31 | '@typescript-eslint/no-empty-function': 'off', 32 | '@typescript-eslint/no-this-alias': 'off', 33 | '@typescript-eslint/ban-types': 'off', 34 | '@typescript-eslint/ban-ts-comment': 'off', 35 | 'prefer-spread': 'off', 36 | 'no-case-declarations': 'off', 37 | 'no-console': 'off', 38 | '@typescript-eslint/no-unused-vars': ['error'], 39 | }, 40 | }; 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "qwikjs-wrap-balancer", 3 | "version": "0.6.0", 4 | "description": "Simple Qwik Component That Makes Titles More Readable", 5 | "main": "./lib/index.qwik.mjs", 6 | "qwik": "./lib/index.qwik.mjs", 7 | "types": "./lib-types/index.d.ts", 8 | "author": { 9 | "email": "harshdev8218@gmail.com", 10 | "name": "Harsh Mangalam" 11 | }, 12 | "repository": { 13 | "url": "https://github.com/harshmangalam/qwik-wrap-balancer" 14 | }, 15 | "exports": { 16 | ".": { 17 | "import": "./lib/index.qwik.mjs", 18 | "require": "./lib/index.qwik.cjs", 19 | "types": "./lib-types/index.d.ts" 20 | } 21 | }, 22 | "files": [ 23 | "lib", 24 | "lib-types" 25 | ], 26 | "keywords": [ 27 | "qwik" 28 | ], 29 | "license": "MIT", 30 | "engines": { 31 | "node": ">=15.0.0" 32 | }, 33 | "private": false, 34 | "type": "module", 35 | "scripts": { 36 | "build": "qwik build", 37 | "build.lib": "vite build --mode lib", 38 | "build.types": "tsc --emitDeclarationOnly", 39 | "dev": "vite --mode ssr", 40 | "dev.debug": "node --inspect-brk ./node_modules/vite/bin/vite.js --mode ssr --force", 41 | "fmt": "prettier --write .", 42 | "fmt.check": "prettier --check .", 43 | "lint": "eslint \"src/**/*.ts*\"", 44 | "release": "np", 45 | "start": "vite --open --mode ssr", 46 | "qwik": "qwik" 47 | }, 48 | "devDependencies": { 49 | "@builder.io/qwik": "1.1.4", 50 | "@types/eslint": "8.4.10", 51 | "@types/node": "^18.11.18", 52 | "@types/node-fetch": "latest", 53 | "@typescript-eslint/eslint-plugin": "5.48.0", 54 | "@typescript-eslint/parser": "5.48.0", 55 | "eslint": "8.31.0", 56 | "eslint-plugin-qwik": "latest", 57 | "node-fetch": "3.3.0", 58 | "np": "7.6.1", 59 | "prettier": "2.8.1", 60 | "typescript": "4.9.4", 61 | "undici": "5.14.0", 62 | "vite": "4.0.3" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/components/wrap-balancer.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | $, 3 | component$, 4 | Slot, 5 | useVisibleTask$, 6 | useSignal, 7 | type HTMLAttributes, 8 | } from "@builder.io/qwik"; 9 | 10 | const SYMBOL_OBSERVER_KEY = "__wrap_o"; 11 | 12 | interface WrapperElement extends HTMLElement { 13 | [SYMBOL_OBSERVER_KEY]?: ResizeObserver | undefined; 14 | } 15 | 16 | interface BalancerProps extends HTMLAttributes { 17 | /** 18 | * The HTML tag to use for the wrapper element. 19 | * @default 'span' 20 | */ 21 | as?: any; 22 | /** 23 | * The balance ratio of the wrapper width (0 <= ratio <= 1). 24 | * 0 means the wrapper width is the same as the container width (no balance, browser default). 25 | * 1 means the wrapper width is the minimum (full balance, most compact). 26 | * @default 1 27 | */ 28 | ratio?: number; 29 | } 30 | 31 | export const WrapBalancer = component$((props: BalancerProps) => { 32 | const { as: Wrapper = "span", ratio = 1 } = props; 33 | const wrapperRef = useSignal(); 34 | 35 | const SYMBOL_KEY = "__wrap_b"; 36 | 37 | const relayout = $((id: string, ratio: number, wrapper: WrapperElement) => { 38 | wrapper = 39 | wrapper || document.querySelector(`[data-br="${id}"]`); 40 | const container = wrapper.parentElement; 41 | 42 | const update = (width: number) => (wrapper.style.maxWidth = width + "px"); 43 | 44 | // Reset wrapper width 45 | wrapper.style.maxWidth = ""; 46 | 47 | // Get the initial container size 48 | const width = container?.clientWidth; 49 | const height = container?.clientHeight; 50 | 51 | // Synchronously do binary search and calculate the layout 52 | let left = (width as number) / 2; 53 | let right = width as number; 54 | let middle: number; 55 | 56 | if (width) { 57 | while (left + 1 < right) { 58 | middle = ~~((left + right) / 2); 59 | update(middle); 60 | if (container.clientHeight === height) { 61 | right = middle; 62 | } else { 63 | left = middle; 64 | } 65 | } 66 | 67 | // Update the wrapper width 68 | update(right * ratio + width * (1 - ratio)); 69 | } 70 | 71 | // Create a new observer if we don't have one. 72 | // Note that we must inline the key here as we use `toString()` to serialize 73 | // the function. 74 | if (!wrapper["__wrap_o"]) { 75 | (wrapper["__wrap_o"] = new ResizeObserver(() => { 76 | (self as any).__wrap_b(0, +(wrapper.dataset.brr as any), wrapper); 77 | })).observe(container as HTMLElement); 78 | } 79 | }); 80 | 81 | useVisibleTask$(({ track }) => { 82 | track(() => ratio); 83 | track(() => wrapperRef.value); 84 | if (wrapperRef.value) { 85 | // Re-assign the function here as the component can be dynamically rendered, and script tag won't work in that case. 86 | ((self as any)[SYMBOL_KEY] = relayout)( 87 | "balancer", 88 | ratio, 89 | wrapperRef.value 90 | ); 91 | } 92 | }); 93 | 94 | return ( 95 | <> 96 | 107 | 108 | 109 | 110 | ); 111 | }); 112 | --------------------------------------------------------------------------------