├── .github └── workflows │ └── website.yml ├── .gitignore ├── .prettierrc ├── Hammer.ts ├── README.md ├── assets ├── hammer.png ├── vike-hammer_favicon.svg └── vike-hammer_with-text.svg ├── editor.html ├── editor ├── index.css └── index.ts ├── embed.html ├── embed ├── index.ts └── style.css ├── fonts ├── Inter-Var.ttf └── Mitr-Light.ttf ├── index.html ├── index ├── anchor-scrolling.css ├── anchor-scrolling.ts └── css │ ├── blockquote.css │ ├── code.css │ ├── copy.css │ ├── fonts.css │ └── index.css ├── package.json ├── pnpm-lock.yaml ├── pricing.html ├── pricing ├── Pricing.css ├── Pricing.tsx ├── PricingDesc.tsx ├── index.css └── index.tsx ├── tsconfig.json └── vite.config.ts /.github/workflows/website.yml: -------------------------------------------------------------------------------- 1 | name: Website 2 | on: 3 | push: 4 | branches: 5 | - main 6 | permissions: 7 | contents: write 8 | jobs: 9 | website: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: pnpm/action-setup@v4 14 | with: 15 | version: 9.10.0 16 | - run: pnpm install 17 | - run: pnpm build 18 | - uses: JamesIves/github-pages-deploy-action@4.1.4 19 | with: 20 | branch: gh-pages 21 | folder: dist/ 22 | # Remove previous build 23 | clean: true 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /dist/ 3 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | semi: false 2 | tabWidth: 2 3 | singleQuote: true 4 | printWidth: 120 5 | trailingComma: all 6 | -------------------------------------------------------------------------------- /Hammer.ts: -------------------------------------------------------------------------------- 1 | export { Hammer } 2 | export { toHumanReadable } 3 | export { fromHumanReadable } 4 | export { fromHumanReadableAxis } 5 | export { colorsDefault } 6 | export type { IlloElement } 7 | export type { Colors } 8 | export type { Perspective } 9 | export type { PerspectiveUserControlable } 10 | 11 | import * as Zdog from 'zdog' 12 | const { TAU } = Zdog 13 | 14 | /*******************************/ 15 | /********** DEFAULTS ***********/ 16 | /*******************************/ 17 | 18 | const headLength = 35 19 | 20 | const handleDiameterDefault = 8.6 21 | const handleLengthDefault = 16.7 22 | 23 | const STROKE = 0 24 | const slopeSize = 3 25 | const sideLength = 8 26 | 27 | // perspectiveDefault is meant to be overriden by setting hammer.perspective = /* ... */ 28 | const perspectiveDefault: Perspective = { 29 | rotate: fromHumanReadable({ x: 0, y: 0, z: 0 }), 30 | translate: { x: 0, y: 0, z: 0 }, 31 | } 32 | 33 | const colorsDefault: Colors = { 34 | metal1: '#ababab', 35 | metal2: '#949494', 36 | metal3: '#757575', 37 | metal4: '#6e6e6e', 38 | metal5: '#7a7a7a', 39 | metal6: '#828282', 40 | wood: '#91512b', 41 | lightningBolt: '#fbbf28', 42 | } 43 | 44 | /******************************/ 45 | /*********** LOGIC ************/ 46 | /******************************/ 47 | 48 | // default to flat, filled shapes 49 | ;[Zdog.Shape, Zdog.Rect, Zdog.Ellipse].forEach(function (ItemClass) { 50 | //@ts-ignore 51 | ItemClass.defaults.fill = true 52 | //@ts-ignore 53 | ItemClass.defaults.stroke = false 54 | }) 55 | 56 | type IlloElement = SVGSVGElement | HTMLCanvasElement 57 | 58 | type Perspective = { 59 | rotate: { 60 | x: number 61 | y: number 62 | z: number 63 | } 64 | translate: { 65 | x: number 66 | y: number 67 | z: number 68 | } 69 | } 70 | type PerspectiveUserControlable = { 71 | x: number 72 | y: number 73 | z: number 74 | } 75 | 76 | type Colors = { 77 | metal1: string 78 | metal2: string 79 | metal3: string 80 | metal4: string 81 | metal5: string 82 | metal6: string 83 | wood: string 84 | lightningBolt: string 85 | } 86 | 87 | class Hammer { 88 | constructor(outerElem: HTMLElement) { 89 | if (!outerElem) throw new Error('Missing `outerElem` argument') 90 | const illoElem = renderOuterHtml(outerElem) 91 | this.illoElem = illoElem 92 | this.colors = colorsDefault 93 | this.perspective = perspectiveDefault 94 | this.handleDiameterDefault = handleDiameterDefault 95 | this.handleDiameter = handleDiameterDefault 96 | this.handleLength = handleLengthDefault 97 | this.handleLengthDefault = handleLengthDefault 98 | this.hideBackLightningBolt = false 99 | } 100 | illo: Zdog.Illustration | undefined = undefined 101 | illoElem: IlloElement 102 | perspective: Perspective 103 | dragRotate: boolean = false 104 | onDragStart: (() => void) | undefined = undefined 105 | handleDiameter: number 106 | handleDiameterDefault: number 107 | handleLength: number 108 | handleLengthDefault: number 109 | hideBackLightningBolt: boolean 110 | reset() { 111 | this.init() 112 | } 113 | updatePerspective() { 114 | if (this.illo) { 115 | Object.assign(this.illo.rotate, this.perspective.rotate) 116 | Object.assign(this.illo.translate, this.perspective.translate) 117 | this.illo.updateRenderGraph() 118 | } 119 | } 120 | colors: Colors 121 | init() { 122 | this.illo = render(this) 123 | this.illo.updateRenderGraph() 124 | } 125 | update() { 126 | if (this.illo) { 127 | this.illo.updateRenderGraph() 128 | } 129 | } 130 | } 131 | 132 | function renderOuterHtml(outerElem: HTMLElement) { 133 | outerElem.style.position = 'relative' 134 | outerElem.innerHTML = ` 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 155 | ` 156 | const illoElem: SVGSVGElement = outerElem.querySelector('svg.hammer')! 157 | illoElem.style.height = '100%' 158 | illoElem.style.width = '100%' 159 | return illoElem 160 | } 161 | 162 | type Options = { 163 | colors: Colors 164 | hideBackLightningBolt: boolean 165 | } 166 | 167 | function render(hammer: Hammer) { 168 | const { illoElem, perspective, dragRotate } = hammer 169 | illoElem.setAttribute('width', '100') 170 | illoElem.setAttribute('height', '100') 171 | const illo = new Zdog.Illustration({ 172 | element: illoElem, 173 | dragRotate, 174 | onDragStart() { 175 | hammer.onDragStart?.() 176 | }, 177 | onDragMove() { 178 | Object.assign(perspective.rotate, illo.rotate) 179 | Object.assign(perspective.translate, illo.translate) 180 | }, 181 | //zoom: 4.3, 182 | rotate: perspective.rotate, 183 | translate: perspective.translate, 184 | }) 185 | 186 | const { colors, hideBackLightningBolt } = hammer 187 | const options = { colors, hideBackLightningBolt } 188 | 189 | // anchor 190 | var hammerGroup = new Zdog.Group({ 191 | addTo: illo, 192 | //translate: { y: -20, x: -20 }, 193 | updateSort: true, 194 | }) 195 | var handle = new Zdog.Anchor({ 196 | addTo: hammerGroup, 197 | //* 198 | rotate: { x: TAU / 4 }, 199 | //*/ 200 | }) 201 | var head = new Zdog.Anchor({ 202 | addTo: hammerGroup, 203 | //* 204 | rotate: { z: (-1 * TAU) / 4 }, 205 | translate: { x: -1 * (headLength / 2), y: -10 }, 206 | //*/ 207 | }) 208 | 209 | genHead(head, options) 210 | 211 | { 212 | const { handleDiameter, handleLength } = hammer 213 | genHandle(handle, colors, handleDiameter, handleLength) 214 | } 215 | 216 | return illo 217 | } 218 | 219 | function genHandle(handle: Zdog.Anchor, colors: Colors, handleDiameter: number, handleLength: number) { 220 | const handleStick = colors.wood 221 | const mountColor1 = colors.metal4 222 | const mountColor2 = colors.metal5 223 | const mountColor3 = colors.metal6 224 | 225 | let zOffset = 0 226 | const mount = (color: string, stroke: number, length: number = 0) => { 227 | stroke = stroke / 2 228 | const zOffsetAddendum = stroke + length 229 | /* 230 | if( zOffset === 0 ) { 231 | } else { 232 | zOffset += zOffsetAddendum 233 | } 234 | */ 235 | zOffset += zOffsetAddendum / 2 236 | new Zdog.Cylinder({ 237 | addTo: handle, 238 | diameter: handleDiameter, 239 | stroke: stroke, 240 | length, 241 | fill: true, 242 | color, 243 | translate: { x: 0, y: 0, z: 0 - 1 - zOffset }, 244 | }) 245 | zOffset += zOffsetAddendum / 2 246 | } 247 | 248 | mount(mountColor1, 1, 3) 249 | mount(handleStick, 0, handleLength) 250 | mount(mountColor2, 1, 1) 251 | mount(mountColor3, 2, 3) 252 | } 253 | 254 | function genHead(head: Zdog.Anchor, options: Options) { 255 | genHeadSides(head, options.colors) 256 | genHeadFaces(head, options) 257 | } 258 | 259 | function genHeadSides(head: Zdog.Anchor, colors: Colors) { 260 | const headSide = genHeadSide(head, colors) 261 | 262 | headSide.copyGraph({ 263 | addTo: head, 264 | rotate: { x: TAU / 2 }, 265 | translate: { y: headLength }, 266 | }) 267 | } 268 | 269 | function genHeadFaces(head: Zdog.Anchor, options: Options) { 270 | genFaces(head, options) 271 | genFaceSlopes(head, options.colors) 272 | } 273 | function genFaces(head: Zdog.Anchor, options: Options) { 274 | const { colors } = options 275 | 276 | const shape = (props: Zdog.ShapeOptions) => 277 | new Zdog.Shape({ 278 | stroke: STROKE, 279 | ...props, 280 | }) 281 | 282 | // Bottom face 283 | const face = shape({ 284 | path: [ 285 | { x: -1, y: 0, z: 0.5 }, 286 | { x: -1, y: 0, z: -0.5 }, 287 | { x: -1, y: 1, z: -0.5 }, 288 | { x: -1, y: 1, z: 0.5 }, 289 | ], 290 | translate: { y: slopeSize }, 291 | scale: { x: sideLength + slopeSize, y: headLength - 2 * slopeSize, z: 2 * sideLength }, 292 | color: colors.metal3, 293 | addTo: head, 294 | }) 295 | 296 | // Upper face 297 | const opposite = 2 * (sideLength + slopeSize) 298 | const face2 = face.copy({ 299 | translate: { x: opposite, y: slopeSize }, 300 | addTo: head, 301 | }) 302 | 303 | // Front face 304 | var frontFaceGroup = new Zdog.Group({ 305 | addTo: head, 306 | }) 307 | face2.copy({ 308 | rotate: { y: (-1 * TAU) / 4 }, 309 | translate: { x: 0, y: slopeSize }, 310 | addTo: frontFaceGroup, 311 | }) 312 | const viteLogo = genViteLogo(frontFaceGroup, colors) 313 | 314 | // Back face 315 | frontFaceGroup.copyGraph({ 316 | rotate: { x: (-1 * TAU) / 2 }, 317 | translate: { x: 0, y: headLength, z: 0 }, 318 | addTo: head, 319 | }) 320 | 321 | if (options.hideBackLightningBolt) { 322 | viteLogo.remove() 323 | } 324 | } 325 | 326 | function genFaceSlopes(head: Zdog.Anchor, colors: Colors) { 327 | const shape = (props: Zdog.ShapeOptions) => 328 | new Zdog.Shape({ 329 | stroke: STROKE, 330 | addTo: head, 331 | ...props, 332 | }) 333 | 334 | const x = -1 * sideLength 335 | const y = headLength - 2 * slopeSize 336 | const z1 = sideLength + slopeSize 337 | const z2 = sideLength 338 | const faceSlope = shape({ 339 | path: [ 340 | { x, y: 0, z: z1 }, 341 | { x: x - slopeSize, y: 0, z: z2 }, 342 | { x: x - slopeSize, y, z: z2 }, 343 | { x, y, z: z1 }, 344 | ], 345 | translate: { y: slopeSize }, 346 | color: colors.metal2, 347 | }) 348 | 349 | const opposite = 2 * sideLength + slopeSize 350 | faceSlope.copy({ 351 | translate: { x: opposite, y: slopeSize, z: -1 * opposite }, 352 | }) 353 | faceSlope.copy({ 354 | rotate: { x: TAU / 2 }, 355 | translate: { y: headLength - slopeSize }, 356 | }) 357 | faceSlope.copy({ 358 | rotate: { x: TAU / 2 }, 359 | translate: { x: opposite, y: headLength - slopeSize, z: 1 * opposite }, 360 | }) 361 | } 362 | 363 | function genHeadSide(head: Zdog.Anchor, colors: Colors) { 364 | const headSide = new Zdog.Anchor({ 365 | addTo: head, 366 | }) 367 | 368 | const shape = (props: Zdog.ShapeOptions) => 369 | new Zdog.Shape({ 370 | stroke: STROKE, 371 | addTo: headSide, 372 | ...props, 373 | }) 374 | 375 | const colorEdge = colors.metal2 376 | const colorCorner = colors.metal1 377 | 378 | // east slope 379 | var EWSlope = shape({ 380 | path: [ 381 | { x: 0, y: 0, z: 1 }, 382 | { x: 0, y: 0, z: -1 }, 383 | { x: 1, y: 1, z: -1 }, 384 | { x: 1, y: 1, z: 1 }, 385 | ], 386 | translate: { x: sideLength }, 387 | scale: { x: slopeSize, y: slopeSize, z: sideLength }, 388 | color: colorEdge, 389 | }) 390 | 391 | // south slope 392 | var NSSLope = shape({ 393 | path: [ 394 | { z: 0, y: 0, x: 1 }, 395 | { z: 0, y: 0, x: -1 }, 396 | { z: 1, y: 1, x: -1 }, 397 | { z: 1, y: 1, x: 1 }, 398 | ], 399 | translate: { z: sideLength }, 400 | scale: { x: sideLength, y: slopeSize, z: slopeSize }, 401 | color: colorEdge, 402 | }) 403 | 404 | // south east corner 405 | shape({ 406 | path: [ 407 | { x: 0, y: 0, z: 0 }, 408 | { x: slopeSize, y: slopeSize, z: 0 }, 409 | { x: 0, y: slopeSize, z: slopeSize }, 410 | ], 411 | translate: { x: sideLength, z: sideLength }, 412 | color: colorCorner, 413 | }) 414 | 415 | // north slope 416 | NSSLope.copy({ 417 | scale: { x: sideLength, y: slopeSize, z: -1 * slopeSize }, 418 | translate: { z: -1 * sideLength }, 419 | color: colorEdge, 420 | }) 421 | 422 | // north east corner 423 | shape({ 424 | path: [ 425 | { x: 0, y: 0, z: 0 }, 426 | { x: slopeSize, y: slopeSize, z: 0 }, 427 | { x: 0, y: slopeSize, z: -1 * slopeSize }, 428 | ], 429 | translate: { x: sideLength, z: -1 * sideLength }, 430 | color: colorCorner, 431 | }) 432 | 433 | // west slope 434 | EWSlope.copy({ 435 | scale: { x: -1 * slopeSize, y: slopeSize, z: sideLength }, 436 | translate: { x: -1 * sideLength }, 437 | color: colorEdge, 438 | }) 439 | 440 | // north west corner 441 | shape({ 442 | path: [ 443 | { x: 0, y: 0, z: 0 }, 444 | { x: -slopeSize, y: slopeSize, z: 0 }, 445 | { x: 0, y: slopeSize, z: -1 * slopeSize }, 446 | ], 447 | translate: { x: -1 * sideLength, z: -1 * sideLength }, 448 | color: colorCorner, 449 | }) 450 | 451 | // south west corner 452 | shape({ 453 | path: [ 454 | { x: 0, y: 0, z: 0 }, 455 | { x: -1 * slopeSize, y: slopeSize, z: 0 }, 456 | { x: 0, y: slopeSize, z: slopeSize }, 457 | ], 458 | translate: { x: -1 * sideLength, z: sideLength }, 459 | color: colorCorner, 460 | }) 461 | 462 | /* Failed attempt to remove aliasing issues 463 | const y = 0.02 464 | */ 465 | const y = 0 466 | 467 | // cover 468 | shape({ 469 | path: [ 470 | { x: -1, y, z: 1 }, 471 | { x: -1, y, z: -1 }, 472 | { x: 1, y, z: -1 }, 473 | { x: 1, y, z: 1 }, 474 | ], 475 | scale: { x: sideLength, y: sideLength, z: sideLength }, 476 | color: colors.metal3, 477 | }) 478 | 479 | return headSide 480 | } 481 | 482 | function genViteLogo(group: Zdog.Group, colors: Colors) { 483 | const scale = 0.04 484 | //const width = 12 485 | 486 | //const translate = {x: 0, y: 0, z: 0} 487 | /*/ 488 | const addTo = illo 489 | const translate = { x: -1 * (headLength / 2) + width / 2 , y: -2 * sideLength, z: sideLength + slopeSize + 2 } 490 | const rotate = undefined; 491 | /*/ 492 | const addTo = group 493 | const translate = { x: 6.5, y: 8.2, z: sideLength + slopeSize } 494 | const rotate = { z: (1 * TAU) / 4 } 495 | //*/ 496 | const stroke = 0.6 497 | // Distance from the hammer => apparent thickness of the Vite logo 498 | const thikness = stroke / 4 499 | const shape = new Zdog.Shape({ 500 | addTo, 501 | rotate, 502 | path: [ 503 | { x: 292.965, y: 1.5744 }, 504 | { 505 | bezier: [ 506 | { x: 292.965, y: 1.5744 }, 507 | { x: 156.801, y: 28.2552 }, 508 | { x: 156.801, y: 28.2552 }, 509 | ], 510 | }, 511 | { 512 | bezier: [ 513 | { x: 154.563, y: 28.6937 }, 514 | { x: 152.906, y: 30.5903 }, 515 | { x: 152.771, y: 32.8664 }, 516 | ], 517 | }, 518 | { 519 | bezier: [ 520 | { x: 152.771, y: 32.8664 }, 521 | { x: 144.395, y: 174.33 }, 522 | { x: 144.395, y: 174.33 }, 523 | ], 524 | }, 525 | { 526 | bezier: [ 527 | { x: 144.198, y: 177.662 }, 528 | { x: 147.258, y: 180.248 }, 529 | { x: 150.51, y: 179.498 }, 530 | ], 531 | }, 532 | { 533 | bezier: [ 534 | { x: 150.51, y: 179.498 }, 535 | { x: 188.42, y: 170.749 }, 536 | { x: 188.42, y: 170.749 }, 537 | ], 538 | }, 539 | { 540 | bezier: [ 541 | { x: 191.967, y: 169.931 }, 542 | { x: 195.172, y: 173.055 }, 543 | { x: 194.443, y: 176.622 }, 544 | ], 545 | }, 546 | { 547 | bezier: [ 548 | { x: 194.443, y: 176.622 }, 549 | { x: 183.18, y: 231.775 }, 550 | { x: 183.18, y: 231.775 }, 551 | ], 552 | }, 553 | { 554 | bezier: [ 555 | { x: 182.422, y: 235.487 }, 556 | { x: 185.907, y: 238.661 }, 557 | { x: 189.532, y: 237.56 }, 558 | ], 559 | }, 560 | { 561 | bezier: [ 562 | { x: 189.532, y: 237.56 }, 563 | { x: 212.947, y: 230.446 }, 564 | { x: 212.947, y: 230.446 }, 565 | ], 566 | }, 567 | { 568 | bezier: [ 569 | { x: 216.577, y: 229.344 }, 570 | { x: 220.065, y: 232.527 }, 571 | { x: 219.297, y: 236.242 }, 572 | ], 573 | }, 574 | { 575 | bezier: [ 576 | { x: 219.297, y: 236.242 }, 577 | { x: 201.398, y: 322.875 }, 578 | { x: 201.398, y: 322.875 }, 579 | ], 580 | }, 581 | { 582 | bezier: [ 583 | { x: 200.278, y: 328.294 }, 584 | { x: 207.486, y: 331.249 }, 585 | { x: 210.492, y: 326.603 }, 586 | ], 587 | }, 588 | { 589 | bezier: [ 590 | { x: 210.492, y: 326.603 }, 591 | { x: 212.5, y: 323.5 }, 592 | { x: 212.5, y: 323.5 }, 593 | ], 594 | }, 595 | { 596 | bezier: [ 597 | { x: 212.5, y: 323.5 }, 598 | { x: 323.454, y: 102.072 }, 599 | { x: 323.454, y: 102.072 }, 600 | ], 601 | }, 602 | { 603 | bezier: [ 604 | { x: 325.312, y: 98.3645 }, 605 | { x: 322.108, y: 94.137 }, 606 | { x: 318.036, y: 94.9228 }, 607 | ], 608 | }, 609 | { 610 | bezier: [ 611 | { x: 318.036, y: 94.9228 }, 612 | { x: 279.014, y: 102.454 }, 613 | { x: 279.014, y: 102.454 }, 614 | ], 615 | }, 616 | { 617 | bezier: [ 618 | { x: 275.347, y: 103.161 }, 619 | { x: 272.227, y: 99.746 }, 620 | { x: 273.262, y: 96.1583 }, 621 | ], 622 | }, 623 | { 624 | bezier: [ 625 | { x: 273.262, y: 96.1583 }, 626 | { x: 298.731, y: 7.86689 }, 627 | { x: 298.731, y: 7.86689 }, 628 | ], 629 | }, 630 | { 631 | bezier: [ 632 | { x: 299.767, y: 4.27314 }, 633 | { x: 296.636, y: 0.855181 }, 634 | { x: 292.965, y: 1.5744 }, 635 | ], 636 | }, 637 | ], 638 | closed: true, 639 | stroke, 640 | fill: true, 641 | color: colors.lightningBolt, 642 | translate: { x: translate.x, y: translate.y, z: translate.z + thikness }, 643 | scale: { x: scale, y: scale, z: scale }, 644 | }) 645 | return shape 646 | } 647 | 648 | function toHumanReadable({ x, y, z }: { x: number; y: number; z: number }) { 649 | const f = (v: number) => { 650 | v = (v * 16) / TAU 651 | v = Math.floor(v * 100) / 100 652 | return v 653 | } 654 | x = f(x) 655 | y = f(y) 656 | z = f(z) 657 | return { x, y, z } 658 | } 659 | function fromHumanReadableAxis(n: number) { 660 | return TAU * (n / 16) 661 | } 662 | function fromHumanReadable({ x, y, z }: { x: number; y: number; z: number }) { 663 | const f = fromHumanReadableAxis 664 | x = f(x) 665 | y = f(y) 666 | z = f(z) 667 | return { x, y, z } 668 | } 669 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Build Your Own Framework 2 | 3 | See [vike.land.dev](https://land.vike.dev). 4 | -------------------------------------------------------------------------------- /assets/hammer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brillout/vike-land/31dd0acf9b5e3e127c550f73627871d3df55b255/assets/hammer.png -------------------------------------------------------------------------------- /assets/vike-hammer_favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /assets/vike-hammer_with-text.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /editor.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 |
9 | 10 |
11 |
12 |
to original values (settings are persisted to localStorage)
13 |
14 |
file vike-generated.svg 15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
Drag'n'drop to change perspective.
33 |
34 |
Embedded: /embed.html.
35 |
36 |
Made with Zdog.
37 |
38 |
39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /editor/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: sans-serif; 4 | --logo-size-large: 400px; 5 | --logo-size-small: 40px; 6 | --logo-size: var(--logo-size-large); 7 | } 8 | body.faviconSize { 9 | --logo-size: var(--logo-size-small); 10 | } 11 | 12 | .hammer { 13 | cursor: move; 14 | } 15 | #logo { 16 | width: var(--logo-size); 17 | height: var(--logo-size); 18 | } 19 | #wrapper { 20 | float: left; 21 | width: var(--logo-size-large); 22 | height: var(--logo-size-large); 23 | } 24 | #colors { 25 | display: flex; 26 | flex-direction: column; 27 | } 28 | label { 29 | display: inline-block; 30 | } 31 | 32 | body.zdogView .circle-square { 33 | display: none; 34 | } 35 | #logo:hover .hammer { 36 | outline: 1px solid black; 37 | } 38 | 39 | code { 40 | font-family: monospace; 41 | background-color: #eaeaea; 42 | padding: 3px 5px; 43 | border-radius: 4px; 44 | } 45 | -------------------------------------------------------------------------------- /editor/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | colorsDefault, 3 | fromHumanReadable, 4 | fromHumanReadableAxis, 5 | Hammer, 6 | toHumanReadable, 7 | type Colors, 8 | type PerspectiveUserControlable, 9 | } from '../Hammer' 10 | 11 | const presetsColor: Record = { 12 | oldest: { 13 | metal1: '#949494', 14 | metal2: '#828282', 15 | metal3: '#696969', 16 | metal4: '#707070', 17 | metal5: '#707070', 18 | metal6: '#696969', 19 | wood: '#96511d', 20 | lightningBolt: '#ecb018', 21 | }, 22 | ['too-bright']: { 23 | metal1: '#bdbdbd', 24 | metal2: '#b3b3b3', 25 | metal3: '#a6a6a6', 26 | metal4: '#9e9e9e', 27 | metal5: '#9e9e9e', 28 | metal6: '#a6a6a6', 29 | wood: '#a56c4a', 30 | lightningBolt: '#fbcc56', 31 | }, 32 | brighter: { 33 | metal1: '#b5b5b5', 34 | metal2: '#949494', 35 | metal3: '#7a7a7a', 36 | metal4: '#787878', 37 | metal5: '#787878', 38 | metal6: '#7a7a7a', 39 | wood: '#91512b', 40 | lightningBolt: '#f7bc26', 41 | }, 42 | older: { 43 | metal1: '#b5b5b5', 44 | metal2: '#949494', 45 | metal3: '#757575', 46 | metal4: '#787878', 47 | metal5: '#787878', 48 | metal6: '#707070', 49 | wood: '#91512b', 50 | lightningBolt: '#f7bc26', 51 | }, 52 | older2: { 53 | metal1: '#b0b0b0', 54 | metal2: '#949494', 55 | metal3: '#757575', 56 | metal4: '#878787', 57 | metal5: '#878787', 58 | metal6: '#808080', 59 | wood: '#91512b', 60 | lightningBolt: '#f7bc26', 61 | }, 62 | darker: { 63 | metal1: '#a3a3a3', 64 | metal2: '#8f8f8f', 65 | metal3: '#757575', 66 | metal4: '#878787', 67 | metal5: '#878787', 68 | metal6: '#808080', 69 | wood: '#91512b', 70 | lightningBolt: '#f7bc26', 71 | }, 72 | previous: { 73 | metal1: '#b5b5b5', 74 | metal2: '#949494', 75 | metal3: '#757575', 76 | metal4: '#6e6e6e', 77 | metal5: '#767676', 78 | metal6: '#828282', 79 | wood: '#91512b', 80 | lightningBolt: '#f7bc26', 81 | }, 82 | default: { ...colorsDefault }, 83 | } 84 | const presets: Record = { 85 | maud: { 86 | rotation2D: 0, 87 | handleLength: 10, 88 | rotate: { x: -0.1, y: -7, z: -2 }, 89 | }, 90 | classic: { 91 | rotation2D: 0, 92 | rotate: { x: -0.13, y: -6.63, z: -1.2 }, 93 | // translate: { x: -2.6, y: 7, z: 0 }, 94 | }, 95 | ['classic-long-handle']: { 96 | rotation2D: 0, 97 | handleLength: 20.5, 98 | rotate: { x: -0.13, y: -6.63, z: -1.2 }, 99 | // translate: { x: -2.6, y: 7, z: 0 }, 100 | }, 101 | ['oldest-mirrored']: { 102 | rotation2D: -21, 103 | rotate: { x: -0.7, y: 7.1, z: 0 }, 104 | }, 105 | previous: { 106 | rotation2D: 11, 107 | rotate: { x: -0.3, y: -6.63, z: 0 }, 108 | }, 109 | new1: { 110 | rotation2D: -23, 111 | rotate: { x: -0.4, y: 152.8, z: 0 }, 112 | }, 113 | new2: { 114 | rotation2D: -23, 115 | rotate: { x: -0.6, y: 159.19, z: 0 }, 116 | }, 117 | new3: { 118 | rotation2D: -23, 119 | rotate: { x: -0.4, y: 159.3, z: 0 }, 120 | }, 121 | favicon: { 122 | rotation2D: 29, 123 | rotate: { x: -0.4, y: 0.6, z: 0 }, 124 | }, 125 | latest: { 126 | rotation2D: -30, 127 | rotate: { x: -0.5, y: 23.2, z: 0 }, 128 | }, 129 | latest2: { 130 | rotation2D: 13, 131 | rotate: { x: -0.4, y: 23.5, z: 0 }, 132 | }, 133 | ['vertical-mirrored']: { 134 | handleDiameter: 8.2, 135 | handleLength: 21.8, 136 | rotation2D: 0, 137 | rotate: { x: -0.4, y: -56.41, z: 0 }, 138 | }, 139 | vertical: { 140 | handleDiameter: 8.2, 141 | handleLength: 21.8, 142 | rotation2D: 0, 143 | rotate: { x: -0.4, y: -55.6, z: 0 }, 144 | }, 145 | laying: { 146 | handleDiameter: 8.5, 147 | handleLength: 20.9, 148 | rotation2D: 66.3, 149 | rotate: { x: -0.4, y: -56.41, z: 0 }, 150 | }, 151 | } 152 | type Preset = { 153 | rotation2D: number 154 | rotate: { x: number; y: number; z: number } 155 | handleDiameter?: number 156 | handleLength?: number 157 | } 158 | 159 | const perspectiveDefault = presets.latest2 160 | 161 | let changeRotation2D: (n: number) => void 162 | let getRotation2D: () => number 163 | let changeHandleDiameter: (n: number) => void 164 | let changeHandleLength: (n: number) => void 165 | let updateColorInputs: () => void 166 | main() 167 | 168 | function getElements() { 169 | return { 170 | presets: document.getElementById('presets')!, 171 | presetsColor: document.getElementById('presetsColor')!, 172 | colorPicker: document.getElementById('colorPicker')!, 173 | handleDiameterPicker: document.getElementById('handleDiameterPicker')!, 174 | handleLengthPicker: document.getElementById('handleLengthPicker')!, 175 | zdogView: document.getElementById('zdogView')!, 176 | faviconSize: document.getElementById('faviconSize')!, 177 | autoSpinning: document.getElementById('autoSpinning')!, 178 | reset: document.querySelector('button#reset') as HTMLButtonElement, 179 | download: document.querySelector('button#download') as HTMLButtonElement, 180 | hideBackLightningBolt: document.getElementById('hideBackLightningBolt')!, 181 | rotateX: document.getElementById('rotate-x')!, 182 | rotateY: document.getElementById('rotate-y')!, 183 | rotateZ: document.getElementById('rotate-z')!, 184 | rotate2D: document.getElementById('rotate2D')!, 185 | translateX: document.getElementById('translate-x')!, 186 | translateY: document.getElementById('translate-y')!, 187 | translateZ: document.getElementById('translate-z')!, 188 | } 189 | } 190 | 191 | function main() { 192 | const elements = getElements() 193 | 194 | const hammer = new Hammer(document.querySelector('#logo')!) 195 | hammer.dragRotate = true 196 | 197 | zdogViewInit(elements.zdogView) 198 | 199 | initPresets(elements.presets, hammer) 200 | initPresetsColor(elements.presetsColor, hammer) 201 | initColorInputs(elements.colorPicker, hammer) 202 | changeHandleDiameter = initHandlePicker(hammer, elements.handleDiameterPicker, 'handleDiameter').changeVal 203 | changeHandleLength = initHandlePicker(hammer, elements.handleLengthPicker, 'handleLength').changeVal 204 | initFaviconSize(elements.faviconSize) 205 | initAutoSpinning(elements.autoSpinning) 206 | initReset(elements.reset) 207 | initDownload(elements.download) 208 | initHighBackLightningBold(elements.hideBackLightningBolt, hammer) 209 | initPerspectiveControlers(hammer, elements) 210 | initRotate2D(elements.rotate2D) 211 | 212 | animate(hammer) 213 | 214 | hammer.init() 215 | } 216 | 217 | function initPerspectiveControlers( 218 | hammer: Hammer, 219 | { 220 | rotateX, 221 | rotateY, 222 | rotateZ, 223 | }: /* 224 | translateX, 225 | translateY, 226 | translateZ, 227 | */ 228 | { 229 | rotateX: HTMLElement 230 | rotateY: HTMLElement 231 | rotateZ: HTMLElement 232 | translateX: HTMLElement 233 | translateY: HTMLElement 234 | translateZ: HTMLElement 235 | }, 236 | ) { 237 | ;( 238 | [ 239 | { 240 | elem: rotateX, 241 | type: 'rotate' as const, 242 | axis: 'x', 243 | }, 244 | { 245 | elem: rotateY, 246 | type: 'rotate' as const, 247 | axis: 'y', 248 | }, 249 | { 250 | elem: rotateZ, 251 | type: 'rotate' as const, 252 | axis: 'z', 253 | }, 254 | /* 255 | { 256 | elem: translateX, 257 | type: 'translate' as const, 258 | axis: 'x', 259 | }, 260 | { 261 | elem: translateY, 262 | type: 'translate' as const, 263 | axis: 'y', 264 | }, 265 | { 266 | elem: translateZ, 267 | type: 'translate' as const, 268 | axis: 'z', 269 | }, 270 | */ 271 | ] as const 272 | ).forEach(({ elem, axis, type }) => { 273 | const getCoordinate = () => hammer.perspective[type] 274 | const { changeVal } = createNumberInput({ 275 | elem, 276 | labelText: `${axis} (${type})`, 277 | getValue() { 278 | const n = toHumanReadable(getCoordinate())[axis] 279 | return n 280 | }, 281 | setValue(n: number) { 282 | getCoordinate()[axis] = fromHumanReadableAxis(n) 283 | }, 284 | defaultValue: perspectiveDefault.rotate[axis], 285 | hammer, 286 | }) 287 | if (type === 'rotate') { 288 | if (!onPerspectiveChange) onPerspectiveChange = [] 289 | onPerspectiveChange.push((perspectiveUserControlable) => { 290 | const n = perspectiveUserControlable[axis] 291 | changeVal(n, true) 292 | }) 293 | } 294 | }) 295 | } 296 | 297 | function initRotate2D(elemRotate2D: HTMLElement) { 298 | const elemLogo = document.getElementById('logo')! 299 | 300 | const toVal = (n: number) => `rotate(${n}deg)` 301 | const fromVal = (val: string) => parseInt(val.split('(')[1]!.split('deg)')[0]!, 10) 302 | 303 | const controls = createNumberInput({ 304 | elem: elemRotate2D, 305 | labelText: `degree (2D rotation)`, 306 | defaultValue: perspectiveDefault.rotation2D, 307 | getValue() { 308 | const val = elemLogo.style.transform 309 | return fromVal(val) 310 | }, 311 | setValue(n: number) { 312 | const val = toVal(n) 313 | elemLogo.style.transform = val 314 | }, 315 | step: 1, 316 | }) 317 | changeRotation2D = controls.changeVal 318 | getRotation2D = controls.getValue 319 | } 320 | 321 | function initHandlePicker(hammer: Hammer, handlePicker: Element, handleProp: 'handleDiameter' | 'handleLength') { 322 | return createNumberInput({ 323 | elem: handlePicker, 324 | labelText: `${handleProp}`, 325 | getValue() { 326 | return hammer[handleProp] 327 | }, 328 | setValue(n: number) { 329 | hammer[handleProp] = n 330 | }, 331 | hammer, 332 | }) 333 | } 334 | 335 | function createNumberInput({ 336 | elem, 337 | labelText, 338 | getValue, 339 | setValue, 340 | defaultValue, 341 | step = 0.1, 342 | hammer, 343 | }: { 344 | elem: Element 345 | labelText: string 346 | getValue: () => number 347 | setValue: (n: number) => void 348 | step?: number 349 | defaultValue?: number 350 | hammer?: Hammer 351 | }) { 352 | const storeKey = elem.id! 353 | 354 | { 355 | const val = toFloat(getStoreValue(storeKey)) 356 | if (val !== null) setValue(val) 357 | else if (defaultValue !== undefined) setValue(defaultValue) 358 | } 359 | 360 | //
361 | elem.innerHTML = '' 362 | const parentEl = document.createElement('div') 363 | elem.appendChild(parentEl) 364 | const labelEl = document.createElement('label') 365 | parentEl.appendChild(labelEl) 366 | const inputEl = document.createElement('input') 367 | inputEl.setAttribute('type', 'number') 368 | inputEl.setAttribute('step', step.toString()) 369 | inputEl.style.width = '40px' 370 | inputEl.style.padding = '4px' 371 | labelEl.appendChild(inputEl) 372 | const valEl = document.createElement('span') 373 | valEl.innerHTML = ' ' + labelText 374 | parentEl.appendChild(valEl) 375 | 376 | const val = getValue() 377 | inputEl.value = String(val) 378 | 379 | const apply = (n: number) => { 380 | setValue(n) 381 | hammer?.reset() 382 | } 383 | 384 | inputEl.oninput = (ev: any) => { 385 | const val: string = ev.target!.value 386 | const n = toFloat(val) 387 | apply(n) 388 | setStoreValue(storeKey, val) 389 | } 390 | 391 | const changeVal = (n: number, alreadyApplied?: true) => { 392 | const val = String(n) 393 | inputEl.value = val 394 | if (!alreadyApplied) apply(n) 395 | setStoreValue(storeKey, val) 396 | } 397 | const controls = { changeVal, getValue } 398 | return controls 399 | } 400 | 401 | function zdogViewInit(zdogView: Element) { 402 | createCheckboxInput({ 403 | elem: zdogView, 404 | labelText: 'Icon view', 405 | onToggle(isChecked: boolean) { 406 | document.body.classList[!isChecked ? 'add' : 'remove']('zdogView') 407 | }, 408 | }) 409 | } 410 | 411 | function initFaviconSize(faviconSize: Element) { 412 | createCheckboxInput({ 413 | elem: faviconSize, 414 | labelText: 'Favicon size', 415 | onToggle(isChecked: boolean) { 416 | document.body.classList[isChecked ? 'add' : 'remove']('faviconSize') 417 | }, 418 | }) 419 | } 420 | 421 | function initAutoSpinning(autoSpinning: Element) { 422 | createCheckboxInput({ 423 | elem: autoSpinning, 424 | labelText: 'Auto spinning', 425 | onToggle(isChecked: boolean) { 426 | isSpinning = isChecked 427 | }, 428 | }) 429 | } 430 | 431 | function initHighBackLightningBold(hideBackLightningBolt: HTMLElement, hammer: Hammer) { 432 | createCheckboxInput({ 433 | elem: hideBackLightningBolt, 434 | labelText: 'Hide back lightning bolt', 435 | onToggle(isChecked: boolean) { 436 | hammer.hideBackLightningBolt = isChecked 437 | }, 438 | applyValue() { 439 | hammer.reset() 440 | }, 441 | }) 442 | } 443 | 444 | function createCheckboxInput({ 445 | elem, 446 | labelText, 447 | onToggle, 448 | applyValue, 449 | }: { 450 | elem: Element 451 | labelText: string 452 | onToggle: (isChecked: boolean) => void 453 | applyValue?: (isChecked: boolean) => void 454 | }) { 455 | elem.innerHTML = '' 456 | const labelEl = document.createElement('label') 457 | elem.appendChild(labelEl) 458 | const inputEl = document.createElement('input') 459 | inputEl.setAttribute('type', 'checkbox') 460 | labelEl.appendChild(inputEl) 461 | labelEl.appendChild(document.createTextNode(labelText)) 462 | 463 | const { id } = elem 464 | assert(id) 465 | const storeGet = () => (JSON.parse(getStoreValue(id) ?? '"{}"').isChecked as undefined | boolean) ?? false 466 | const storeToggle = () => { 467 | let isChecked = storeGet() 468 | isChecked = !isChecked 469 | setStoreValue(id, JSON.stringify({ isChecked })) 470 | } 471 | const updateUI = (isInit?: true) => { 472 | const isChecked = storeGet() 473 | onToggle(isChecked) 474 | inputEl.checked = isChecked 475 | if (!isInit) { 476 | applyValue?.(isChecked) 477 | } 478 | } 479 | inputEl.oninput = (ev) => { 480 | ev.preventDefault() 481 | storeToggle() 482 | updateUI() 483 | } 484 | updateUI(true) 485 | } 486 | 487 | function initReset(reset: HTMLButtonElement) { 488 | reset.onclick = () => { 489 | isSpinning = false 490 | clearStore() 491 | // @ts-ignore 492 | window.navigation.reload() 493 | } 494 | } 495 | function initDownload(download: HTMLButtonElement) { 496 | download.onclick = () => { 497 | const hammerSvg = document.querySelector('.hammer')! 498 | let content = hammerSvg.outerHTML 499 | content = content.replace('', '') 503 | downloadFile(content, 'image/svg+xml', 'vike-generated.svg') 504 | } 505 | } 506 | 507 | function downloadFile(content: string, mimeType: string, filename: string) { 508 | const a = document.createElement('a') 509 | const blob = new Blob([content], { type: mimeType }) 510 | const url = URL.createObjectURL(blob) 511 | a.setAttribute('href', url) 512 | a.setAttribute('download', filename) 513 | a.click() 514 | } 515 | 516 | function initPresets(presetsEl: Element, hammer: Hammer) { 517 | addHeading('Settings', presetsEl) 518 | Object.entries(presets).forEach(([name, preset]) => { 519 | genPresetBtn(name, presetsEl, () => { 520 | changeHandleDiameter(preset.handleDiameter || hammer.handleDiameterDefault) 521 | changeHandleLength(preset.handleLength || hammer.handleLengthDefault) 522 | hammer.perspective.rotate = fromHumanReadable(preset.rotate) 523 | changeRotation2D(preset.rotation2D) 524 | hammer.reset() 525 | }) 526 | }) 527 | } 528 | function initPresetsColor(presetsColorEl: Element, hammer: Hammer) { 529 | addHeading('Colors', presetsColorEl) 530 | Object.entries(presetsColor).forEach(([name, colors]) => { 531 | genPresetBtn(name, presetsColorEl, () => { 532 | hammer.colors = colors 533 | hammer.reset() 534 | updateColorInputs() 535 | }) 536 | }) 537 | } 538 | 539 | function genPresetBtn(name: string, parentEl: Element, onclick: () => void) { 540 | const btnEl = document.createElement('button') 541 | btnEl.style.marginRight = '10px' 542 | btnEl.style.marginBottom = '7px' 543 | btnEl.innerHTML = ` ${name} ` 544 | btnEl.onclick = onclick 545 | parentEl.appendChild(btnEl) 546 | } 547 | 548 | function addHeading(heading: string, el: Element) { 549 | const headingEl = document.createElement('h3') 550 | headingEl.innerHTML = heading 551 | el.prepend(headingEl) 552 | } 553 | 554 | function initColorInputs(colorPicker: Element, hammer: Hammer) { 555 | colorPicker.innerHTML = '' 556 | 557 | const updateInputs: (() => void)[] = [] 558 | updateColorInputs = () => updateInputs.forEach((f) => f()) 559 | 560 | objectKeys(hammer.colors).forEach((key) => { 561 | { 562 | const val = getStoreValue(key) 563 | if (val) hammer.colors[key] = val 564 | } 565 | 566 | //
567 | const parentEl = document.createElement('div') 568 | colorPicker.appendChild(parentEl) 569 | const labelEl = document.createElement('label') 570 | parentEl.appendChild(labelEl) 571 | const inputEl = document.createElement('input') 572 | inputEl.setAttribute('type', 'color') 573 | labelEl.appendChild(inputEl) 574 | const valEl = document.createElement('span') 575 | parentEl.appendChild(valEl) 576 | 577 | const updateInput = () => { 578 | const val = hammer.colors[key] 579 | inputEl.value = val 580 | valEl.innerHTML = ` ${val} ${key}` 581 | } 582 | const updateStore = () => { 583 | const val = hammer.colors[key] 584 | setStoreValue(key, val) 585 | } 586 | updateInput() 587 | updateInputs.push(() => { 588 | updateInput() 589 | updateStore() 590 | }) 591 | 592 | inputEl.oninput = (ev: any) => { 593 | const val: string = ev.target!.value 594 | hammer.colors[key] = val 595 | updateInput() 596 | hammer.reset() 597 | updateStore() 598 | } 599 | }) 600 | } 601 | 602 | var isSpinning: boolean 603 | var isMouseover: boolean 604 | var onPerspectiveChange: ((perspective: PerspectiveUserControlable) => void | undefined)[] 605 | function animate(hammer: Hammer) { 606 | const logoElem = document.getElementById('logo')! 607 | logoElem.onmouseleave = () => { 608 | isMouseover = false 609 | } 610 | logoElem.onmouseenter = () => { 611 | isMouseover = true 612 | } 613 | /* 614 | hammer.onDragStart = () => { 615 | isMouseover = true 616 | } 617 | */ 618 | render(hammer) 619 | } 620 | 621 | function render(hammer: Hammer) { 622 | requestAnimationFrame(() => { 623 | if (isSpinning && !isMouseover) { 624 | hammer.perspective.rotate.y += 0.015 625 | hammer.updatePerspective() 626 | } 627 | hammer.update() 628 | callOnPerspectiveChange(hammer) 629 | render(hammer) 630 | }) 631 | } 632 | 633 | var rotateValue: string 634 | function callOnPerspectiveChange(hammer: Hammer) { 635 | if (!hammer.illo) return 636 | const { x, y, z } = toHumanReadable(hammer.illo.rotate) 637 | const rotateValueNew = JSON.stringify({ x, y, z }, null, 2) 638 | const hasChanged = rotateValue !== rotateValueNew 639 | rotateValue = rotateValueNew 640 | if (!hasChanged) return 641 | const perspectiveUserControlable: PerspectiveUserControlable = { x, y, z } 642 | onPerspectiveChange.forEach((fn) => fn(perspectiveUserControlable)) 643 | } 644 | 645 | function assert(condition: unknown): asserts condition { 646 | if (!condition) throw new Error('Assertion failed.') 647 | } 648 | 649 | function getStoreValue(key: string): null | string { 650 | return window.localStorage[`__vike_logo__input_${key}`] ?? null 651 | } 652 | function setStoreValue(key: string, val: string): void | undefined { 653 | window.localStorage[`__vike_logo__input_${key}`] = val 654 | } 655 | function clearStore() { 656 | window.localStorage.clear() 657 | } 658 | 659 | function toFloat(val: string): number 660 | function toFloat(val: string | null): number | null 661 | function toFloat(val: string | null): number | null { 662 | if (val === null) return null 663 | return parseFloat(val) 664 | } 665 | 666 | /** Same as Object.keys() but with type inference */ 667 | export function objectKeys>(obj: T): Array { 668 | return Object.keys(obj) 669 | } 670 | -------------------------------------------------------------------------------- /embed.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 |

Vike

11 |
12 | 13 |
14 | 15 |

Vike

16 |
17 | 18 |
19 | 20 |

Vike

21 |
22 | 23 |
24 | 25 |

Vike

26 |
27 | 28 |
29 | 30 |

Vike

31 |
32 | 33 |
34 | 35 |

Vike

36 |
37 | 38 |
39 |
40 |
41 |
42 |
43 |
44 | Logo Editor 45 |
46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /embed/index.ts: -------------------------------------------------------------------------------- 1 | import { fromHumanReadable, Hammer, type Perspective } from '../Hammer' 2 | 3 | const perspective: Perspective = { 4 | rotate: fromHumanReadable({ x: -0.13, y: -6.63, z: -1.2 }), 5 | translate: { x: -2.6, y: 7, z: 0 }, 6 | } 7 | 8 | Array.from(document.querySelectorAll('.logo')).forEach((logoEl) => { 9 | const hammer = new Hammer(logoEl as HTMLElement) 10 | hammer.perspective = perspective 11 | hammer.init() 12 | }) 13 | -------------------------------------------------------------------------------- /embed/style.css: -------------------------------------------------------------------------------- 1 | .header { 2 | text-align: center; 3 | } 4 | .header > * { 5 | display: inline-block; 6 | vertical-align: middle; 7 | } 8 | .header > .logo { 9 | height: 200px; 10 | width: 200px; 11 | position: relative; 12 | top: -13px; 13 | right: -25px; 14 | } 15 | 16 | body { 17 | font-family: 'Inter'; 18 | } 19 | button { 20 | font-family: inherit; 21 | } 22 | body { 23 | line-height: 1.5; 24 | } 25 | 26 | @font-face { 27 | font-family: 'Inter'; 28 | font-weight: 100 900; 29 | font-stretch: 75% 125%; 30 | font-style: oblique 0deg 12deg; 31 | src: url('/fonts/Inter-Var.ttf') format('truetype-variations'); 32 | } 33 | 34 | @font-face { 35 | font-family: 'Mitr'; 36 | font-weight: 100 900; 37 | src: url('/fonts/Mitr-Light.ttf') format('truetype-variations'); 38 | } 39 | 40 | #header-0 { 41 | position: relative; 42 | left: -76px; 43 | } 44 | #header-0 #logo-png { 45 | width: 85px; 46 | margin-right: 27px; 47 | } 48 | 49 | #header-2 { 50 | color: white; 51 | --color-border: black; 52 | -webkit-text-stroke: 3px black; 53 | } 54 | 55 | /* Adapted from https://markdotto.com/playground/3d-text/ */ 56 | #header-5 { 57 | background-color: #0e8cbb; 58 | } 59 | #header-5 > .title { 60 | color: white; 61 | /* prettier-ignore */ 62 | text-shadow: 63 | 0 1px 0 #ccc, 64 | 0 2px 0 #c9c9c9, 65 | 0 3px 0 #bbb, 66 | 0 4px 0 #b9b9b9, 67 | 0 5px 0 #aaa, 68 | 0 6px 1px rgba(0,0,0,.1), 69 | 0 0 5px rgba(0,0,0,.1), 70 | 0 1px 3px rgba(0,0,0,.3), 71 | 0 3px 5px rgba(0,0,0,.2), 72 | 0 5px 10px rgba(0,0,0,.25), 73 | 0 10px 10px rgba(0,0,0,.2), 74 | 0 20px 20px rgba(0,0,0,.15); 75 | } 76 | 77 | #header-3 > .title { 78 | color: #ddd; 79 | --step: 20; 80 | --color-side: black; 81 | --color-border: black; 82 | -webkit-text-stroke: 2px black; 83 | /* prettier-ignore */ 84 | text-shadow: 85 | 0 1px 0 var(--color-side), 86 | 0 2px 0 var(--color-side), 87 | 0 3px 0 var(--color-side), 88 | 0 4px 0 var(--color-side), 89 | 0 5px 0 var(--color-side), 90 | 0 6px 0 var(--color-side); 91 | } 92 | #header-4 > .title { 93 | color: black; 94 | --step: 100; 95 | --color-side: rgba(var(--step), var(--step), var(--step), 0.3); 96 | /* prettier-ignore */ 97 | text-shadow: 98 | 0 1px 0 var(--color-side), 99 | 0 2px 0 var(--color-side), 100 | 0 3px 0 var(--color-side), 101 | 0 4px 0 var(--color-side), 102 | 0 5px 0 var(--color-side), 103 | 0 6px 0 var(--color-side); 104 | } 105 | .header > .title { 106 | font-size: 5em; 107 | font: bold 100px/1 'Helvetica Neue', Helvetica, Arial, sans-serif; 108 | } 109 | .header { 110 | position: relative; 111 | --shift: 120px; 112 | width: calc(100vw + var(--shift)); 113 | left: calc(-1 * var(--shift)); 114 | } 115 | 116 | body { 117 | margin: 0; 118 | padding-bottom: 100px; 119 | } 120 | 121 | .circle-square { 122 | display: none; 123 | } 124 | -------------------------------------------------------------------------------- /fonts/Inter-Var.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brillout/vike-land/31dd0acf9b5e3e127c550f73627871d3df55b255/fonts/Inter-Var.ttf -------------------------------------------------------------------------------- /fonts/Mitr-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brillout/vike-land/31dd0acf9b5e3e127c550f73627871d3df55b255/fonts/Mitr-Light.ttf -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Vike: Build Your Own Framework 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 |

14 | Vike 19 | 20 |
21 | Build Your Own Framework 22 |
23 |

24 |
25 | 26 |
27 | 34 | 35 | 36 | 37 |

It has never been easier to build your own framework

38 |

39 | Building a framework like Next.js used to be hard. These days are over: with modern tools, such as Vite and its 40 | ecosystem, you can build your own Next.js with only hundreds of lines of code. 41 |

42 |

43 | Don't let a framework dictate your architecture, keep full control, and use any tool and technology you want. 44 |

45 |

46 | Build internal company frameworks to scale your teams, or enhance your product with a bespoke framework to 47 | delight your users, or just keep architectural control. 48 |

49 |

Build Your Own Framework.

50 | 51 |

All-included frameworks

52 |

53 | While frameworks like Next.js only take care of the frontend, you can build a framework that includes 54 | everything: the server (Express.js, Koa, ...), authentication, API (GraphQL, RPC, ...), ORM (Prisma, MikroORM, 55 | ...), logging (e.g. Prometheus), error tracking (Sentry, Bugsnag, ...), emailing (e.g. Mailgun + Nodemailer), 56 | deployment (AWS, Cloudflare Workers, ...), etc. 57 |

58 |

59 | All your framework's users have left to do is: define UI components, and define data models (if your framework 60 | has a database). 61 |

62 | 63 |

Build company internal frameworks, to scale your teams

64 |

65 | Give senior employees the power of fully owning the architecture while providing junior employees an 66 | all-included framework that takes care of everything. 67 |

68 |

Keep full control and leverage your company's favorite tools without limitations.

69 |

70 | You can then share (some of) your internal frameworks with the world, fostering open source values within your 71 | company while stimulating and attracting engineering talent. For example, Facebook's decision to open source and 72 | share React with the world has been a strong force within Facebook for engineering excellence and has enabled 73 | Facebook to hire highly skilled Software Engineers. 74 |

75 | 76 |

Build a framework tailored to your product, to delight your users

77 |

Delight your users with a bespoke framework that deeply integrates with your product.

78 |

79 | For example, if you are a headless CMS provider (Sanity, Contentful, Tina, ...), build a CMS on top. Or, if you 80 | are a full-stack deployment provider (PlanetScale, Cloudflare D1, Fly, ...), build a little full-stack framework 81 | to showcase the capabalities of your deployment platform and for the community to fork. Or, if you are an 82 | e-commerce platform, provide a framework like Shopify's 83 | Hydrogen. 84 |

85 |

Build a "Framework as a Product".

86 | 87 |

Getting Started

88 |

Vike is just a website (this website) recommending you to build your own framework.

89 |

We recommend following tools.

90 |
    91 |
  • 92 | Vite - Next Generation Frontend Tooling. 93 |
    94 |

    Vite is a webpack alternative that has first-class support for building frameworks.

    95 |

    96 | In particular, check out Vite's native SSR API which is a 97 | low-level and highly flexible API enabling you to build SSR frameworks. 98 |

    99 |
    100 |
  • 101 |
  • 102 | vite-plugin-ssr - Like Next.js/Nuxt but as do-one-thing-do-it-well 103 | Vite plugin. 104 |
    105 |

    106 | Vite-plugin-ssr also has first-class support for building framworks. It takes care of significantly more 107 | things than Vite's native SSR API and enables you to build a framework like Next.js with only hundreds of 108 | lines of code. 109 |

    110 |

    That said, if you need more flexibility then use Vite's native SSR API instead.

    111 |

    112 | Check out its guide 113 | vite-plugin-ssr > Build Your Own Framework. 116 |

    117 |
    118 |
  • 119 |
  • 120 | react-streaming - React 18 Streaming. Full-fledged & 121 | Easy. 122 |
    123 |

    124 | Easily integrate React 18 SSR streaming advanced capabilities such as server-side 125 | <Suspense>. 126 |

    127 |
    128 |
  • 129 |
  • 130 | HatTip - (nothing) Like Express.js 131 |
    132 |

    Easily integrate your framework with any deployment provider (Cloudflare Workers, Vercel, ...).

    133 |
    134 |
  • 135 |
136 |
137 | 138 | 139 | 153 | 154 | 155 | 156 | 157 | 158 | -------------------------------------------------------------------------------- /index/anchor-scrolling.css: -------------------------------------------------------------------------------- 1 | h1, 2 | h2[id] { 3 | cursor: pointer; 4 | } 5 | h2[id] { 6 | --anchor-icon-width: 1em; 7 | position: relative; 8 | margin-left: calc(-1 * var(--anchor-icon-width)); 9 | padding-left: var(--anchor-icon-width); 10 | } 11 | h2[id]:hover:before { 12 | content: '#'; 13 | position: absolute; 14 | left: 0px; 15 | color: #aaa; 16 | } 17 | -------------------------------------------------------------------------------- /index/anchor-scrolling.ts: -------------------------------------------------------------------------------- 1 | import './anchor-scrolling.css' 2 | 3 | anchorScrolling() 4 | 5 | // Copied & adapted from https://stackoverflow.com/questions/7717527/smooth-scrolling-when-clicking-an-anchor-link/49910424#49910424 6 | function anchorScrolling() { 7 | document.querySelectorAll('a[href^="#"]').forEach((anchor) => { 8 | anchor.addEventListener('click', function (ev) { 9 | const hash = anchor.getAttribute('href')! 10 | scrollTo(hash) 11 | ev.preventDefault() 12 | }) 13 | }) 14 | 15 | document.querySelectorAll('h2[id]').forEach((heading) => { 16 | heading.addEventListener('click', function (ev) { 17 | const hash = heading.getAttribute('id')! 18 | scrollTo(`#${hash}`) 19 | ev.preventDefault() 20 | }) 21 | }) 22 | 23 | document.querySelector('h1')?.addEventListener('click', () => { 24 | document.documentElement.scrollIntoView({ 25 | behavior: 'smooth', 26 | }) 27 | if (window.location.hash === '' && window.scrollY === 0) { 28 | window.location.reload() 29 | } else { 30 | history.pushState(null, '', '/') 31 | } 32 | }) 33 | } 34 | 35 | function scrollTo(hash: string) { 36 | const el = document.querySelector(hash)! 37 | el.scrollIntoView({ 38 | behavior: 'smooth', 39 | }) 40 | history.pushState(null, '', hash) 41 | } 42 | -------------------------------------------------------------------------------- /index/css/blockquote.css: -------------------------------------------------------------------------------- 1 | blockquote { 2 | border-left: 5px solid #ccc; 3 | border-radius: 5px; 4 | margin: 0; 5 | padding: 12px 18px; 6 | background-color: #f0f0f0; 7 | margin: 6px 0; 8 | } 9 | 10 | blockquote p:first-of-type { 11 | margin-top: 0px; 12 | } 13 | blockquote p:last-of-type { 14 | margin-bottom: 0px; 15 | } 16 | 17 | /* 18 | blockquote.info::before { 19 | content: '\2139\FE0F'; 20 | font-size: 1.1em; 21 | font-family: emoji; 22 | } 23 | */ 24 | -------------------------------------------------------------------------------- /index/css/code.css: -------------------------------------------------------------------------------- 1 | code { 2 | font-family: monospace; 3 | /* 4 | background-color: #f3f3f3; 5 | */ 6 | background-color: #ddd; 7 | padding-left: 6px; 8 | padding-right: 5px; 9 | padding-top: 2px; 10 | padding-bottom: 3px; 11 | font-size: 1.1em; 12 | border-radius: 5px; 13 | } 14 | -------------------------------------------------------------------------------- /index/css/copy.css: -------------------------------------------------------------------------------- 1 | #copy { 2 | max-width: 770px; 3 | width: 100vw; 4 | padding: 0 10px; 5 | margin: auto; 6 | box-sizing: border-box; 7 | } 8 | #copy h2 { 9 | font-weight: 500; 10 | margin-top: 50px; 11 | } 12 | -------------------------------------------------------------------------------- /index/css/fonts.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: 'Inter'; 3 | } 4 | button { 5 | font-family: inherit; 6 | } 7 | body { 8 | line-height: 1.5; 9 | } 10 | 11 | @font-face { 12 | font-family: 'Inter'; 13 | font-weight: 100 900; 14 | font-stretch: 75% 125%; 15 | font-style: oblique 0deg 12deg; 16 | src: url('/fonts/Inter-Var.ttf') format('truetype-variations'); 17 | } 18 | -------------------------------------------------------------------------------- /index/css/index.css: -------------------------------------------------------------------------------- 1 | @import './fonts.css'; 2 | @import './code.css'; 3 | @import './blockquote.css'; 4 | @import './copy.css'; 5 | 6 | body { 7 | margin: 0; 8 | } 9 | 10 | #toc { 11 | margin-top: 20px; 12 | } 13 | #toc a { 14 | margin-bottom: 2px; 15 | display: block; 16 | font-weight: 500; 17 | } 18 | 19 | a { 20 | color: #999; 21 | text-decoration: none; 22 | } 23 | 24 | #tool-list li { 25 | margin: 25px 0px; 26 | } 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "dev": "vite", 4 | "build": "vite build" 5 | }, 6 | "dependencies": { 7 | "@types/node": "^17.0.42", 8 | "@types/react": "^18.2.11", 9 | "@types/react-dom": "^18.2.4", 10 | "@types/zdog": "^1.1.2", 11 | "react": "^18.2.0", 12 | "react-dom": "^18.2.0", 13 | "vite": "^4.3.9", 14 | "zdog": "1.1.3" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '9.0' 2 | 3 | settings: 4 | autoInstallPeers: true 5 | excludeLinksFromLockfile: false 6 | 7 | importers: 8 | 9 | .: 10 | dependencies: 11 | '@types/node': 12 | specifier: ^17.0.42 13 | version: 17.0.42 14 | '@types/react': 15 | specifier: ^18.2.11 16 | version: 18.2.11 17 | '@types/react-dom': 18 | specifier: ^18.2.4 19 | version: 18.2.4 20 | '@types/zdog': 21 | specifier: ^1.1.2 22 | version: 1.1.2 23 | react: 24 | specifier: ^18.2.0 25 | version: 18.2.0 26 | react-dom: 27 | specifier: ^18.2.0 28 | version: 18.2.0(react@18.2.0) 29 | vite: 30 | specifier: ^4.3.9 31 | version: 4.3.9(@types/node@17.0.42) 32 | zdog: 33 | specifier: 1.1.3 34 | version: 1.1.3 35 | 36 | packages: 37 | 38 | '@esbuild/android-arm64@0.17.19': 39 | resolution: {integrity: sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==} 40 | engines: {node: '>=12'} 41 | cpu: [arm64] 42 | os: [android] 43 | 44 | '@esbuild/android-arm@0.17.19': 45 | resolution: {integrity: sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==} 46 | engines: {node: '>=12'} 47 | cpu: [arm] 48 | os: [android] 49 | 50 | '@esbuild/android-x64@0.17.19': 51 | resolution: {integrity: sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==} 52 | engines: {node: '>=12'} 53 | cpu: [x64] 54 | os: [android] 55 | 56 | '@esbuild/darwin-arm64@0.17.19': 57 | resolution: {integrity: sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==} 58 | engines: {node: '>=12'} 59 | cpu: [arm64] 60 | os: [darwin] 61 | 62 | '@esbuild/darwin-x64@0.17.19': 63 | resolution: {integrity: sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==} 64 | engines: {node: '>=12'} 65 | cpu: [x64] 66 | os: [darwin] 67 | 68 | '@esbuild/freebsd-arm64@0.17.19': 69 | resolution: {integrity: sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==} 70 | engines: {node: '>=12'} 71 | cpu: [arm64] 72 | os: [freebsd] 73 | 74 | '@esbuild/freebsd-x64@0.17.19': 75 | resolution: {integrity: sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==} 76 | engines: {node: '>=12'} 77 | cpu: [x64] 78 | os: [freebsd] 79 | 80 | '@esbuild/linux-arm64@0.17.19': 81 | resolution: {integrity: sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==} 82 | engines: {node: '>=12'} 83 | cpu: [arm64] 84 | os: [linux] 85 | 86 | '@esbuild/linux-arm@0.17.19': 87 | resolution: {integrity: sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==} 88 | engines: {node: '>=12'} 89 | cpu: [arm] 90 | os: [linux] 91 | 92 | '@esbuild/linux-ia32@0.17.19': 93 | resolution: {integrity: sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==} 94 | engines: {node: '>=12'} 95 | cpu: [ia32] 96 | os: [linux] 97 | 98 | '@esbuild/linux-loong64@0.17.19': 99 | resolution: {integrity: sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==} 100 | engines: {node: '>=12'} 101 | cpu: [loong64] 102 | os: [linux] 103 | 104 | '@esbuild/linux-mips64el@0.17.19': 105 | resolution: {integrity: sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==} 106 | engines: {node: '>=12'} 107 | cpu: [mips64el] 108 | os: [linux] 109 | 110 | '@esbuild/linux-ppc64@0.17.19': 111 | resolution: {integrity: sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==} 112 | engines: {node: '>=12'} 113 | cpu: [ppc64] 114 | os: [linux] 115 | 116 | '@esbuild/linux-riscv64@0.17.19': 117 | resolution: {integrity: sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==} 118 | engines: {node: '>=12'} 119 | cpu: [riscv64] 120 | os: [linux] 121 | 122 | '@esbuild/linux-s390x@0.17.19': 123 | resolution: {integrity: sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==} 124 | engines: {node: '>=12'} 125 | cpu: [s390x] 126 | os: [linux] 127 | 128 | '@esbuild/linux-x64@0.17.19': 129 | resolution: {integrity: sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==} 130 | engines: {node: '>=12'} 131 | cpu: [x64] 132 | os: [linux] 133 | 134 | '@esbuild/netbsd-x64@0.17.19': 135 | resolution: {integrity: sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==} 136 | engines: {node: '>=12'} 137 | cpu: [x64] 138 | os: [netbsd] 139 | 140 | '@esbuild/openbsd-x64@0.17.19': 141 | resolution: {integrity: sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==} 142 | engines: {node: '>=12'} 143 | cpu: [x64] 144 | os: [openbsd] 145 | 146 | '@esbuild/sunos-x64@0.17.19': 147 | resolution: {integrity: sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==} 148 | engines: {node: '>=12'} 149 | cpu: [x64] 150 | os: [sunos] 151 | 152 | '@esbuild/win32-arm64@0.17.19': 153 | resolution: {integrity: sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==} 154 | engines: {node: '>=12'} 155 | cpu: [arm64] 156 | os: [win32] 157 | 158 | '@esbuild/win32-ia32@0.17.19': 159 | resolution: {integrity: sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==} 160 | engines: {node: '>=12'} 161 | cpu: [ia32] 162 | os: [win32] 163 | 164 | '@esbuild/win32-x64@0.17.19': 165 | resolution: {integrity: sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==} 166 | engines: {node: '>=12'} 167 | cpu: [x64] 168 | os: [win32] 169 | 170 | '@types/node@17.0.42': 171 | resolution: {integrity: sha512-Q5BPGyGKcvQgAMbsr7qEGN/kIPN6zZecYYABeTDBizOsau+2NMdSVTar9UQw21A2+JyA2KRNDYaYrPB0Rpk2oQ==} 172 | 173 | '@types/prop-types@15.7.5': 174 | resolution: {integrity: sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==} 175 | 176 | '@types/react-dom@18.2.4': 177 | resolution: {integrity: sha512-G2mHoTMTL4yoydITgOGwWdWMVd8sNgyEP85xVmMKAPUBwQWm9wBPQUmvbeF4V3WBY1P7mmL4BkjQ0SqUpf1snw==} 178 | 179 | '@types/react@18.2.11': 180 | resolution: {integrity: sha512-+hsJr9hmwyDecSMQAmX7drgbDpyE+EgSF6t7+5QEBAn1tQK7kl1vWZ4iRf6SjQ8lk7dyEULxUmZOIpN0W5baZA==} 181 | 182 | '@types/scheduler@0.16.3': 183 | resolution: {integrity: sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==} 184 | 185 | '@types/zdog@1.1.2': 186 | resolution: {integrity: sha512-E1nINLaVvxu7N66gANMjhlxMdGL9KqsePa7NyO4p96xeQ7YiAjydVH+eE4W2aLdGyIcLqt+pNMK8l7+gD8c+Wg==} 187 | 188 | csstype@3.1.2: 189 | resolution: {integrity: sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==} 190 | 191 | esbuild@0.17.19: 192 | resolution: {integrity: sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==} 193 | engines: {node: '>=12'} 194 | hasBin: true 195 | 196 | fsevents@2.3.2: 197 | resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} 198 | engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} 199 | os: [darwin] 200 | 201 | js-tokens@4.0.0: 202 | resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} 203 | 204 | loose-envify@1.4.0: 205 | resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} 206 | hasBin: true 207 | 208 | nanoid@3.3.6: 209 | resolution: {integrity: sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==} 210 | engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} 211 | hasBin: true 212 | 213 | picocolors@1.0.0: 214 | resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} 215 | 216 | postcss@8.4.24: 217 | resolution: {integrity: sha512-M0RzbcI0sO/XJNucsGjvWU9ERWxb/ytp1w6dKtxTKgixdtQDq4rmx/g8W1hnaheq9jgwL/oyEdH5Bc4WwJKMqg==} 218 | engines: {node: ^10 || ^12 || >=14} 219 | 220 | react-dom@18.2.0: 221 | resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==} 222 | peerDependencies: 223 | react: ^18.2.0 224 | 225 | react@18.2.0: 226 | resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==} 227 | engines: {node: '>=0.10.0'} 228 | 229 | rollup@3.25.1: 230 | resolution: {integrity: sha512-tywOR+rwIt5m2ZAWSe5AIJcTat8vGlnPFAv15ycCrw33t6iFsXZ6mzHVFh2psSjxQPmI+xgzMZZizUAukBI4aQ==} 231 | engines: {node: '>=14.18.0', npm: '>=8.0.0'} 232 | hasBin: true 233 | 234 | scheduler@0.23.0: 235 | resolution: {integrity: sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==} 236 | 237 | source-map-js@1.0.2: 238 | resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} 239 | engines: {node: '>=0.10.0'} 240 | 241 | vite@4.3.9: 242 | resolution: {integrity: sha512-qsTNZjO9NoJNW7KnOrgYwczm0WctJ8m/yqYAMAK9Lxt4SoySUfS5S8ia9K7JHpa3KEeMfyF8LoJ3c5NeBJy6pg==} 243 | engines: {node: ^14.18.0 || >=16.0.0} 244 | hasBin: true 245 | peerDependencies: 246 | '@types/node': '>= 14' 247 | less: '*' 248 | sass: '*' 249 | stylus: '*' 250 | sugarss: '*' 251 | terser: ^5.4.0 252 | peerDependenciesMeta: 253 | '@types/node': 254 | optional: true 255 | less: 256 | optional: true 257 | sass: 258 | optional: true 259 | stylus: 260 | optional: true 261 | sugarss: 262 | optional: true 263 | terser: 264 | optional: true 265 | 266 | zdog@1.1.3: 267 | resolution: {integrity: sha512-raRj6r0gPzopFm5XWBJZr/NuV4EEnT4iE+U3dp5FV5pCb588Gmm3zLIp/j9yqqcMiHH8VNQlerLTgOqL7krh6w==} 268 | 269 | snapshots: 270 | 271 | '@esbuild/android-arm64@0.17.19': 272 | optional: true 273 | 274 | '@esbuild/android-arm@0.17.19': 275 | optional: true 276 | 277 | '@esbuild/android-x64@0.17.19': 278 | optional: true 279 | 280 | '@esbuild/darwin-arm64@0.17.19': 281 | optional: true 282 | 283 | '@esbuild/darwin-x64@0.17.19': 284 | optional: true 285 | 286 | '@esbuild/freebsd-arm64@0.17.19': 287 | optional: true 288 | 289 | '@esbuild/freebsd-x64@0.17.19': 290 | optional: true 291 | 292 | '@esbuild/linux-arm64@0.17.19': 293 | optional: true 294 | 295 | '@esbuild/linux-arm@0.17.19': 296 | optional: true 297 | 298 | '@esbuild/linux-ia32@0.17.19': 299 | optional: true 300 | 301 | '@esbuild/linux-loong64@0.17.19': 302 | optional: true 303 | 304 | '@esbuild/linux-mips64el@0.17.19': 305 | optional: true 306 | 307 | '@esbuild/linux-ppc64@0.17.19': 308 | optional: true 309 | 310 | '@esbuild/linux-riscv64@0.17.19': 311 | optional: true 312 | 313 | '@esbuild/linux-s390x@0.17.19': 314 | optional: true 315 | 316 | '@esbuild/linux-x64@0.17.19': 317 | optional: true 318 | 319 | '@esbuild/netbsd-x64@0.17.19': 320 | optional: true 321 | 322 | '@esbuild/openbsd-x64@0.17.19': 323 | optional: true 324 | 325 | '@esbuild/sunos-x64@0.17.19': 326 | optional: true 327 | 328 | '@esbuild/win32-arm64@0.17.19': 329 | optional: true 330 | 331 | '@esbuild/win32-ia32@0.17.19': 332 | optional: true 333 | 334 | '@esbuild/win32-x64@0.17.19': 335 | optional: true 336 | 337 | '@types/node@17.0.42': {} 338 | 339 | '@types/prop-types@15.7.5': {} 340 | 341 | '@types/react-dom@18.2.4': 342 | dependencies: 343 | '@types/react': 18.2.11 344 | 345 | '@types/react@18.2.11': 346 | dependencies: 347 | '@types/prop-types': 15.7.5 348 | '@types/scheduler': 0.16.3 349 | csstype: 3.1.2 350 | 351 | '@types/scheduler@0.16.3': {} 352 | 353 | '@types/zdog@1.1.2': {} 354 | 355 | csstype@3.1.2: {} 356 | 357 | esbuild@0.17.19: 358 | optionalDependencies: 359 | '@esbuild/android-arm': 0.17.19 360 | '@esbuild/android-arm64': 0.17.19 361 | '@esbuild/android-x64': 0.17.19 362 | '@esbuild/darwin-arm64': 0.17.19 363 | '@esbuild/darwin-x64': 0.17.19 364 | '@esbuild/freebsd-arm64': 0.17.19 365 | '@esbuild/freebsd-x64': 0.17.19 366 | '@esbuild/linux-arm': 0.17.19 367 | '@esbuild/linux-arm64': 0.17.19 368 | '@esbuild/linux-ia32': 0.17.19 369 | '@esbuild/linux-loong64': 0.17.19 370 | '@esbuild/linux-mips64el': 0.17.19 371 | '@esbuild/linux-ppc64': 0.17.19 372 | '@esbuild/linux-riscv64': 0.17.19 373 | '@esbuild/linux-s390x': 0.17.19 374 | '@esbuild/linux-x64': 0.17.19 375 | '@esbuild/netbsd-x64': 0.17.19 376 | '@esbuild/openbsd-x64': 0.17.19 377 | '@esbuild/sunos-x64': 0.17.19 378 | '@esbuild/win32-arm64': 0.17.19 379 | '@esbuild/win32-ia32': 0.17.19 380 | '@esbuild/win32-x64': 0.17.19 381 | 382 | fsevents@2.3.2: 383 | optional: true 384 | 385 | js-tokens@4.0.0: {} 386 | 387 | loose-envify@1.4.0: 388 | dependencies: 389 | js-tokens: 4.0.0 390 | 391 | nanoid@3.3.6: {} 392 | 393 | picocolors@1.0.0: {} 394 | 395 | postcss@8.4.24: 396 | dependencies: 397 | nanoid: 3.3.6 398 | picocolors: 1.0.0 399 | source-map-js: 1.0.2 400 | 401 | react-dom@18.2.0(react@18.2.0): 402 | dependencies: 403 | loose-envify: 1.4.0 404 | react: 18.2.0 405 | scheduler: 0.23.0 406 | 407 | react@18.2.0: 408 | dependencies: 409 | loose-envify: 1.4.0 410 | 411 | rollup@3.25.1: 412 | optionalDependencies: 413 | fsevents: 2.3.2 414 | 415 | scheduler@0.23.0: 416 | dependencies: 417 | loose-envify: 1.4.0 418 | 419 | source-map-js@1.0.2: {} 420 | 421 | vite@4.3.9(@types/node@17.0.42): 422 | dependencies: 423 | '@types/node': 17.0.42 424 | esbuild: 0.17.19 425 | postcss: 8.4.24 426 | rollup: 3.25.1 427 | optionalDependencies: 428 | fsevents: 2.3.2 429 | 430 | zdog@1.1.3: {} 431 | -------------------------------------------------------------------------------- /pricing.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 |
9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /pricing/Pricing.css: -------------------------------------------------------------------------------- 1 | #pricing-page { 2 | width: 1100px; 3 | margin: auto; 4 | } 5 | 6 | #pricing-container { 7 | display: flex; 8 | } 9 | 10 | .pricing-table { 11 | display: table; 12 | font-size: 1.07em; 13 | border-spacing: 40px 8px; 14 | margin: auto; 15 | } 16 | .pricing-row { 17 | display: table-row; 18 | } 19 | .pricing-cell { 20 | display: table-cell; 21 | /* 22 | padding: 20px; 23 | */ 24 | } 25 | 26 | .pricing-header { 27 | font-size: 1.45em; 28 | margin: 7px 30px; 29 | margin-bottom: 20px; 30 | font-weight: 500; 31 | text-align: center; 32 | } 33 | 34 | .pricing-price, 35 | .pricing-free { 36 | font-size: 1.3rem; 37 | font-weight: 600; 38 | } 39 | .pricing-price { 40 | color: blue; 41 | } 42 | .pricing-free { 43 | color: green; 44 | } 45 | 46 | .pricing-price-container { 47 | color: #777; 48 | line-height: 1.4em; 49 | } 50 | .pricing-price-container > * { 51 | display: inline; 52 | vertical-align: middle; 53 | } 54 | .pricing-price-container { 55 | font-size: 0.99rem; 56 | } 57 | .pricing-price-note { 58 | /* 59 | line-height: 0; 60 | */ 61 | } 62 | 63 | .pricing-box { 64 | border-radius: 10px; 65 | border: 1px solid #ddd; 66 | padding: 20px; 67 | margin: 10px; 68 | } 69 | /* 70 | #pricing-individual.pricing-box { 71 | width: 400px; 72 | } 73 | #pricing-company.pricing-box { 74 | width: 400px; 75 | } 76 | */ 77 | 78 | /* 79 | h2 { 80 | font-weight: 500; 81 | font-size: 1.3em; 82 | } 83 | */ 84 | -------------------------------------------------------------------------------- /pricing/Pricing.tsx: -------------------------------------------------------------------------------- 1 | export { Pricing } 2 | 3 | import './Pricing.css' 4 | import React from 'react' 5 | 6 | function Pricing() { 7 | return ( 8 |
9 | 10 | 11 |
12 | ) 13 | } 14 | 15 | function PricingIndividual() { 16 | return ( 17 | <> 18 |

19 |

Individual

20 |

21 |

22 |

Always

23 |

24 |

25 |

26 |

Free

27 |

28 |

29 |

30 |

31 | 32 | ) 33 | } 34 | function PricingCompany() { 35 | return ( 36 | <> 37 |

38 |

Company

39 |

40 |

41 |

Prototyping

42 |

Development

43 |

Finished

44 |

45 |

46 |

47 |

Free

48 |

49 |

50 |

51 |

$49

/ month 52 |
53 |

per paid dev

54 |

55 |

56 |

57 |

Free

58 |

59 |

60 |

61 |

62 | 63 | ) 64 | } 65 | 66 | function P({ c, ...props }: { c: string } & Record) { 67 | return
68 | } 69 | -------------------------------------------------------------------------------- /pricing/PricingDesc.tsx: -------------------------------------------------------------------------------- 1 | export { PricingDesc } 2 | 3 | import React from 'react' 4 | 5 | function PricingDesc() { 6 | return ( 7 |
8 | {/* 9 |

Accessible to everyone

10 | */} 11 |
12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /pricing/index.css: -------------------------------------------------------------------------------- 1 | @import '../index/css/fonts.css'; 2 | 3 | body { 4 | margin: 0; 5 | } 6 | -------------------------------------------------------------------------------- /pricing/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom/client' 3 | 4 | import { Pricing } from './Pricing' 5 | import { PricingDesc } from './PricingDesc' 6 | 7 | const container = document.getElementById('react-root') 8 | const root = ReactDOM.createRoot(container!) 9 | root.render( 10 |
11 |

Pricing

12 | 13 | 14 |
, 15 | ) 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "module": "ES2020", 5 | "moduleResolution": "Node", 6 | "target": "ES2020", 7 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 8 | "types": ["vite/client"], 9 | "jsx": "react", 10 | "skipLibCheck": true, 11 | "esModuleInterop": true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | // vite.config.js 2 | import path from 'path' 3 | import type { UserConfig } from 'vite' 4 | 5 | const config: UserConfig = { 6 | server: { 7 | host: true, 8 | port: 3000 9 | }, 10 | build: { 11 | rollupOptions: { 12 | input: { 13 | index: path.resolve(__dirname, 'index.html'), 14 | editor: path.resolve(__dirname, 'editor.html'), 15 | embed: path.resolve(__dirname, 'embed.html') 16 | } 17 | } 18 | } 19 | } 20 | export default config 21 | --------------------------------------------------------------------------------