fragment-tools 0.2.9 → 0.2.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/package.json +5 -6
  2. package/src/cli/createConfig.js +38 -19
  3. package/src/cli/plugins/hot-shader-replacement.js +0 -5
  4. package/src/cli/plugins/save.js +7 -2
  5. package/src/cli/run.js +27 -6
  6. package/src/client/app/lib/canvas-recorder/CanvasRecorder.js +3 -1
  7. package/src/client/app/lib/canvas-recorder/GIFRecorder.js +11 -1
  8. package/src/client/app/lib/canvas-recorder/MediaBunnyRecorder.js +97 -0
  9. package/src/client/app/modules/Exports.svelte +28 -4
  10. package/src/client/app/state/exports.svelte.js +40 -2
  11. package/src/client/app/state/rendering.svelte.js +1 -1
  12. package/src/client/app/ui/Field.svelte +7 -3
  13. package/src/client/app/ui/FieldSection.svelte +1 -1
  14. package/src/client/app/ui/LayoutBuild.svelte +1 -1
  15. package/src/client/app/ui/SketchRenderer.svelte +5 -1
  16. package/src/client/app/ui/fields/CheckboxInput.svelte +9 -7
  17. package/src/client/app/ui/fields/ImageInput.svelte +33 -15
  18. package/src/client/app/ui/fields/Select.svelte +2 -1
  19. package/src/client/app/utils/canvas.utils.js +21 -19
  20. package/src/index.js +12 -0
  21. package/src/types/config.d.ts +6 -0
  22. package/src/types/index.d.ts +1 -0
  23. package/src/types/props.d.ts +32 -5
  24. package/.changeset/README.md +0 -8
  25. package/.changeset/config.json +0 -11
  26. package/.prettierignore +0 -5
  27. package/.prettierrc +0 -25
  28. package/CHANGELOG.md +0 -10
  29. package/src/client/app/lib/canvas-recorder/MP4Recorder.js +0 -44
  30. package/src/client/app/lib/canvas-recorder/WebMRecorder.js +0 -29
  31. package/src/client/app/lib/canvas-recorder/mp4.js +0 -1654
  32. package/src/client/app/lib/canvas-recorder/mp4.wasm +0 -0
  33. package/src/client/public/preview.html +0 -59
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "fragment-tools",
3
- "version": "0.2.9",
3
+ "version": "0.2.11",
4
4
  "description": "A web development environment for creative coding",
5
- "main": "index.js",
5
+ "main": "./src/index.js",
6
6
  "bin": {
7
7
  "fragment": "bin/index.js"
8
8
  },
@@ -26,22 +26,21 @@
26
26
  "dependencies": {
27
27
  "@clack/core": "^0.4.2",
28
28
  "@sveltejs/vite-plugin-svelte": "^5.0.3",
29
- "body-parser": "^2.2.0",
30
29
  "changedpi": "^1.0.4",
31
30
  "convert-length": "^1.0.1",
32
31
  "get-port": "^7.1.0",
33
32
  "gifenc": "^1.0.3",
34
- "glslify": "^7.1.1",
35
33
  "is-unicode-supported": "^2.0.0",
36
34
  "kleur": "^4.1.4",
35
+ "mediabunny": "^1.13.3",
36
+ "milliparsec": "^5.1.0",
37
37
  "sade": "^1.7.4",
38
38
  "svelte": "^5.19.0",
39
39
  "vite": "^6.3.5",
40
- "webm-writer": "^1.0.0",
41
40
  "ws": "^8.2.3"
42
41
  },
43
42
  "devDependencies": {
44
- "@changesets/cli": "^2.29.6",
43
+ "@changesets/cli": "^2.29.7",
45
44
  "@svitejs/changesets-changelog-github-compact": "^1.2.0",
46
45
  "@types/p5": "^1.7.6",
47
46
  "@types/three": "^0.174.0",
@@ -1,5 +1,6 @@
1
1
  import path from 'node:path';
2
2
  import fs from 'node:fs';
3
+ import url from 'node:url';
3
4
  import { defineConfig, loadConfigFromFile, mergeConfig } from 'vite';
4
5
  import { svelte } from '@sveltejs/vite-plugin-svelte';
5
6
 
@@ -8,30 +9,47 @@ import { __dirname, file } from './utils.js';
8
9
  import { log } from './log.js';
9
10
  import sketches from './plugins/sketches.js';
10
11
 
12
+ /**
13
+ *
14
+ * @param {{ cwd: string, filepath: string | undefined }} params
15
+ * @returns {Promise<import('../types/config.js').Config>}
16
+ */
11
17
  export async function loadConfig({ cwd, filepath }) {
12
18
  try {
13
- let filename = `fragment.config.js`;
14
- let configFile = filepath ? filepath : filename;
15
19
  let configRoot = cwd;
16
- let resolvedPath = path.resolve(cwd, configFile);
20
+ let filenames = [`fragment.config.js`, `fragment.config.ts`];
17
21
 
18
- if (!fs.existsSync(resolvedPath)) {
19
- if (filepath) {
20
- log.error(`Config file not found: ${resolvedPath}`);
21
- }
22
- return {};
22
+ if (filepath) {
23
+ filenames = [filepath, ...filenames];
23
24
  }
24
25
 
25
- log.info(`Extending configuration from ${resolvedPath}`);
26
+ let filepaths = filenames.map((filename) => {
27
+ return path.resolve(configRoot, filename);
28
+ });
26
29
 
27
- const { config } = await loadConfigFromFile(
28
- {
29
- command: 'build',
30
- mode: 'dev',
31
- },
32
- configFile,
33
- configRoot,
30
+ let resolvedIndex = filepaths.findIndex((filepath) =>
31
+ fs.existsSync(filepath),
34
32
  );
33
+ /** @type {string|undefined} */
34
+ let resolvedPath = filepaths[resolvedIndex];
35
+
36
+ if (filepath && resolvedIndex !== 0) {
37
+ log.error(`Config file not found: ${filepath}`);
38
+ }
39
+
40
+ if (!resolvedPath) {
41
+ return {};
42
+ }
43
+
44
+ let configFile = path.relative(cwd, resolvedPath);
45
+
46
+ log.info(`Extending configuration from ${configFile}`);
47
+
48
+ const config = (
49
+ await import(
50
+ `${url.pathToFileURL(configFile).href}?ts=${Date.now()}`
51
+ )
52
+ ).default;
35
53
 
36
54
  return config;
37
55
  } catch (error) {
@@ -43,9 +61,10 @@ export async function loadConfig({ cwd, filepath }) {
43
61
  /**
44
62
  * Create Vite config from entries
45
63
  * @param {string[]} entries
46
- * @param {options} options
64
+ * @param {object} [options]
47
65
  * @param {boolean} [options.dev=false]
48
66
  * @param {boolean} [options.build=false]
67
+ * @param {string} [configFilepath]
49
68
  * @param {string} [cwd=process.cwd()]
50
69
  * @returns {import('vite').UserConfig}
51
70
  */
@@ -63,8 +82,8 @@ export async function createConfig(
63
82
  log.info(`Creating Vite configuration...`);
64
83
 
65
84
  const config = await loadConfig({
66
- filepath: configFilepath,
67
85
  cwd,
86
+ filepath: configFilepath,
68
87
  });
69
88
 
70
89
  return mergeConfig(
@@ -110,7 +129,7 @@ export async function createConfig(
110
129
  __DEV__: !build,
111
130
  },
112
131
  optimizeDeps: {
113
- include: ['convert-length', 'webm-writer', 'changedpi'],
132
+ include: ['convert-length', 'changedpi'],
114
133
  },
115
134
  }),
116
135
  config.vite ?? {},
@@ -1,7 +1,6 @@
1
1
  import path from 'node:path';
2
2
  import fs from 'node:fs';
3
3
  import { readFile } from 'node:fs/promises';
4
- import glslify from 'glslify';
5
4
  import { log, dim, green, yellow } from '../log.js';
6
5
 
7
6
  /**
@@ -232,10 +231,6 @@ ${keyword}${shaderParts[1]}
232
231
  shaderPath,
233
232
  );
234
233
 
235
- code = glslify(code, {
236
- basedir: process.cwd(),
237
- });
238
-
239
234
  if (server) {
240
235
  code = addShaderFilepath(code, shaderPath);
241
236
  }
@@ -1,6 +1,6 @@
1
1
  import path from 'node:path';
2
2
  import { writeFile } from 'node:fs/promises';
3
- import bodyParser from 'body-parser';
3
+ import { json } from 'milliparsec';
4
4
  import { log, green, red } from '../log.js';
5
5
  import { mkdirp } from '../utils.js';
6
6
 
@@ -50,7 +50,12 @@ export default function screenshot({
50
50
  return {
51
51
  name: 'save',
52
52
  configureServer(server) {
53
- server.middlewares.use(bodyParser.json({ limit: '100mb' }));
53
+ const payloadLimit = 100 * 1024 * 1024; // 100mb
54
+ server.middlewares.use(
55
+ json({
56
+ payloadLimit,
57
+ }),
58
+ );
54
59
  server.middlewares.use('/save', async (req, res, next) => {
55
60
  if (req.method === 'POST') {
56
61
  const { files } = req.body;
package/src/cli/run.js CHANGED
@@ -26,17 +26,23 @@ import hotShaderReplacement from './plugins/hot-shader-replacement.js';
26
26
  */
27
27
  export async function run(entry, options = {}) {
28
28
  let fragmentServer;
29
+ /** @type {import('node:fs').FSWatcher} */
30
+ let watcher;
29
31
 
30
32
  const cwd = process.cwd();
31
33
  const command = `run`;
32
34
  const prefix = log.prefix(command);
35
+
36
+ const stop = () => {
37
+ fragmentServer?.close();
38
+ watcher?.close();
39
+ };
40
+
33
41
  const exit = () => {
34
42
  process.off('SIGTERM', exit);
35
43
  process.off('exit', exit);
36
44
 
37
- if (fragmentServer) {
38
- fragmentServer.close();
39
- }
45
+ stop();
40
46
 
41
47
  console.log();
42
48
  };
@@ -62,9 +68,10 @@ export async function run(entry, options = {}) {
62
68
  );
63
69
  }
64
70
 
71
+ const hasTSFiles = entries.some((entry) => entry.endsWith('ts'));
65
72
  const tsConfigDirpath = path.join(cwd, FRAGMENT_DIRECTORY);
66
73
  const tsConfigFilepath = path.join(tsConfigDirpath, 'tsconfig.json');
67
- if (!fs.existsSync(tsConfigFilepath)) {
74
+ if (!fs.existsSync(tsConfigFilepath) && hasTSFiles) {
68
75
  await createTsConfigFile(cwd);
69
76
  }
70
77
 
@@ -106,6 +113,22 @@ export async function run(entry, options = {}) {
106
113
  ],
107
114
  }),
108
115
  );
116
+
117
+ watcher = fs.watch(cwd, (eventType, filename) => {
118
+ if (
119
+ ['fragment.config.js', 'fragment.config.ts'].includes(
120
+ filename,
121
+ ) ||
122
+ options.configFilepath?.includes(filename)
123
+ ) {
124
+ log.warn(`${filename} has changed. Restarting...`);
125
+ console.log();
126
+ server.close();
127
+ stop();
128
+ run(entry, options);
129
+ }
130
+ });
131
+
109
132
  await server.listen();
110
133
 
111
134
  // line break after logs
@@ -130,8 +153,6 @@ export async function run(entry, options = {}) {
130
153
 
131
154
  // line break before fragment logs
132
155
  log.message();
133
-
134
- return server;
135
156
  } catch (error) {
136
157
  // line break before error
137
158
  log.message();
@@ -7,6 +7,7 @@ class CanvasRecorder {
7
7
  duration = Infinity,
8
8
  framerate = 25,
9
9
  quality = 100,
10
+ format,
10
11
  onStart = noop,
11
12
  onTick = noop,
12
13
  onComplete = noop,
@@ -16,6 +17,7 @@ class CanvasRecorder {
16
17
  this.framerate = framerate;
17
18
  this.duration = duration;
18
19
  this.quality = quality;
20
+ this.format = format;
19
21
  this.onStart = onStart;
20
22
  this.onTick = onTick;
21
23
  this.onComplete = onComplete;
@@ -24,7 +26,7 @@ class CanvasRecorder {
24
26
  this.deltaTime = 1000 / this.framerate;
25
27
 
26
28
  this.frameDuration = 1000 / this.framerate;
27
- this.frameTotal = isFinite(duration)
29
+ this.frameTotal = isFinite(this.duration)
28
30
  ? this.duration * this.framerate
29
31
  : Infinity;
30
32
  this.started = false;
@@ -9,7 +9,17 @@ class GIFRecorder extends CanvasRecorder {
9
9
  this.tmpCanvas = document.createElement('canvas');
10
10
  this.tmpContext = this.tmpCanvas.getContext('2d');
11
11
 
12
- this.maxColors = Math.floor(map(this.quality, 1, 100, 1, 256));
12
+ this.maxColors = Math.floor(map(this.quality, 20, 100, 32, 256));
13
+
14
+ if (this.framerate > 50) {
15
+ console.warn(`GIFRecorder :: recording was capped at 50fps.`);
16
+ this.framerate = 50;
17
+ this.deltaTime = 1000 / this.framerate;
18
+ this.frameDuration = 1000 / this.framerate;
19
+ this.frameTotal = isFinite(this.duration)
20
+ ? this.duration * this.framerate
21
+ : Infinity;
22
+ }
13
23
 
14
24
  super.start();
15
25
  }
@@ -0,0 +1,97 @@
1
+ import CanvasRecorder from './CanvasRecorder.js';
2
+ import {
3
+ Output,
4
+ Mp4OutputFormat,
5
+ MkvOutputFormat,
6
+ MovOutputFormat,
7
+ WebMInputFormat,
8
+ BufferTarget,
9
+ CanvasSource,
10
+ Quality,
11
+ QUALITY_VERY_LOW,
12
+ QUALITY_LOW,
13
+ QUALITY_MEDIUM,
14
+ QUALITY_HIGH,
15
+ QUALITY_VERY_HIGH,
16
+ WebMOutputFormat,
17
+ } from 'mediabunny';
18
+ import { map } from '@fragment/utils/math.utils.js';
19
+ import { VIDEO_FORMATS } from '@fragment/state/exports.svelte.js';
20
+
21
+ class MediaBunnyRecorder extends CanvasRecorder {
22
+ /** @type Quality[] */
23
+ static BITRATES = [
24
+ QUALITY_VERY_LOW,
25
+ QUALITY_LOW,
26
+ QUALITY_MEDIUM,
27
+ QUALITY_HIGH,
28
+ QUALITY_VERY_HIGH,
29
+ ];
30
+
31
+ /**
32
+ *
33
+ * @param {HTMLCanvasElement} canvas
34
+ * @param {object} options
35
+ * @param {codec} options.string
36
+ */
37
+ constructor(canvas, { codec, ...options }) {
38
+ super(canvas, options);
39
+
40
+ /** @type {string} */
41
+ this.codec = codec;
42
+
43
+ const outputFormats = new Map();
44
+ outputFormats.set(VIDEO_FORMATS.MKV, MkvOutputFormat);
45
+ outputFormats.set(VIDEO_FORMATS.MP4, Mp4OutputFormat);
46
+ outputFormats.set(VIDEO_FORMATS.MOV, MovOutputFormat);
47
+ outputFormats.set(VIDEO_FORMATS.WEBM, WebMOutputFormat);
48
+
49
+ const outputFormat = outputFormats.get(this.format);
50
+
51
+ /** @type {Output} */
52
+ this.output = new Output({
53
+ format: new outputFormat(),
54
+ target: new BufferTarget(),
55
+ });
56
+
57
+ const { BITRATES } = MediaBunnyRecorder;
58
+
59
+ const bitrate =
60
+ BITRATES[
61
+ Math.floor(map(this.quality, 1, 100, 0, BITRATES.length - 1))
62
+ ];
63
+
64
+ /** @type {CanvasSource} */
65
+ this.videoSource = new CanvasSource(this.canvas, {
66
+ codec,
67
+ bitrate,
68
+ });
69
+
70
+ this.output.addVideoTrack(this.videoSource, {
71
+ frameRate: this.framerate,
72
+ });
73
+ }
74
+
75
+ async load() {
76
+ await this.output.start();
77
+ }
78
+
79
+ async tick({ frameCount, time }) {
80
+ const timestamp = frameCount / this.framerate;
81
+
82
+ this.videoSource.add(timestamp, this.frameDuration / 1000);
83
+ }
84
+
85
+ async end() {
86
+ await this.output.finalize();
87
+
88
+ const { mimeType } = this.output.format;
89
+ const { buffer } = this.output.target;
90
+
91
+ this.result = new Blob([buffer], { type: mimeType });
92
+
93
+ super.end();
94
+ }
95
+ }
96
+
97
+ export default MediaBunnyRecorder;
@@ -7,6 +7,7 @@
7
7
  IMAGE_ENCODINGS,
8
8
  VIDEO_FORMATS,
9
9
  exports,
10
+ getCodecsForFormat,
10
11
  } from '../state/exports.svelte';
11
12
 
12
13
  let { id = layout.getID(), headless = false } = $props();
@@ -81,16 +82,39 @@
81
82
  params={{ options: Object.values(VIDEO_FORMATS) }}
82
83
  onchange={(value) => {
83
84
  exports.videoFormat = value;
85
+
86
+ if (
87
+ !getCodecsForFormat(exports.videoFormat).includes(
88
+ exports.videoCodec,
89
+ )
90
+ ) {
91
+ exports.videoCodec = getCodecsForFormat(
92
+ exports.videoFormat,
93
+ )?.[0];
94
+ }
84
95
  }}
85
96
  />
97
+ {#if [VIDEO_FORMATS.MKV, VIDEO_FORMATS.MP4, VIDEO_FORMATS.WEBM, VIDEO_FORMATS.MOV].includes(exports.videoFormat)}
98
+ <Field
99
+ key="codec"
100
+ value={exports.videoCodec}
101
+ params={{ options: getCodecsForFormat(exports.videoFormat) }}
102
+ onchange={(value) => {
103
+ exports.videoCodec = value;
104
+ }}
105
+ />
106
+ {/if}
86
107
  <Field
87
108
  key="quality"
88
109
  value={exports.videoQuality}
89
110
  params={{
90
- min: 1,
91
- max: 100,
92
- step: 1,
93
- suffix: '%',
111
+ options: [
112
+ { value: 100, label: 'very high' },
113
+ { value: 80, label: 'high' },
114
+ { value: 60, label: 'medium' },
115
+ { value: 40, label: 'low' },
116
+ { value: 20, label: 'very low' },
117
+ ],
94
118
  triggerable: false,
95
119
  }}
96
120
  onchange={(value) => {
@@ -6,13 +6,47 @@ export const IMAGE_ENCODINGS = ['png', 'jpeg', 'webp'];
6
6
  export const VIDEO_FORMATS = {
7
7
  FRAMES: 'frames',
8
8
  MP4: 'mp4',
9
- GIF: 'gif',
10
9
  WEBM: 'webm',
10
+ GIF: 'gif',
11
+ MKV: 'mkv',
12
+ MOV: 'mov',
13
+ };
14
+
15
+ export const VIDEO_CODECS = {
16
+ AVC: 'avc',
17
+ HEVC: 'hevc',
18
+ VP8: 'vp8',
19
+ VP9: 'vp9',
20
+ AV1: 'av1',
11
21
  };
12
22
 
23
+ export const VIDEO_CODECS_FORMATS = new Map();
24
+ VIDEO_CODECS_FORMATS.set(VIDEO_CODECS.AVC, ['mp4', 'mkv', 'mov']);
25
+ VIDEO_CODECS_FORMATS.set(VIDEO_CODECS.HEVC, ['mp4', 'mkv', 'mov']);
26
+ VIDEO_CODECS_FORMATS.set(VIDEO_CODECS.VP8, ['webm', 'mkv']);
27
+ VIDEO_CODECS_FORMATS.set(VIDEO_CODECS.VP9, ['webm', 'mkv']);
28
+ VIDEO_CODECS_FORMATS.set(VIDEO_CODECS.AV1, ['webm', 'mkv']);
29
+
30
+ /**
31
+ * List valid codecs based on format
32
+ * @param {string} format
33
+ * @return {string[]}
34
+ */
35
+ export function getCodecsForFormat(format) {
36
+ const codecs = [];
37
+
38
+ for (const [codec, formats] of VIDEO_CODECS_FORMATS) {
39
+ if (formats.includes(format)) {
40
+ codecs.push(codec);
41
+ }
42
+ }
43
+
44
+ return codecs;
45
+ }
46
+
13
47
  class Exports {
14
48
  imageEncoding = $state(IMAGE_ENCODINGS[0]);
15
- videoFormat = $state(Object.values(VIDEO_FORMATS)[0]);
49
+ videoFormat = $state(VIDEO_FORMATS.MP4);
16
50
  pixelsPerInch = $state(72);
17
51
  framerate = $state(60);
18
52
  useDuration = $state(true);
@@ -20,6 +54,7 @@ class Exports {
20
54
  loopCount = $state(1);
21
55
  imageQuality = $state(100);
22
56
  videoQuality = $state(100);
57
+ videoCodec = $state(VIDEO_CODECS.HEVC);
23
58
  imageCount = $state(1);
24
59
  recording = $state(false);
25
60
  capturing = $state(false);
@@ -41,6 +76,7 @@ class Exports {
41
76
  loopCount: this.loopCount,
42
77
  imageQuality: this.imageQuality,
43
78
  videoQuality: this.videoQuality,
79
+ videoCodec: this.videoCodec,
44
80
  imageCount: this.imageCount,
45
81
  videoCollapsed: this.videoCollapsed,
46
82
  imageCollapsed: this.imageCollapsed,
@@ -105,6 +141,7 @@ class Exports {
105
141
  format = this.videoFormat,
106
142
  imageEncoding = this.imageEncoding,
107
143
  quality = this.videoQuality,
144
+ codec = this.videoCodec,
108
145
  duration,
109
146
  filename,
110
147
  pattern,
@@ -133,6 +170,7 @@ class Exports {
133
170
  onTick,
134
171
  framerate,
135
172
  format,
173
+ codec,
136
174
  imageEncoding,
137
175
  quality,
138
176
  duration: duration * this.loopCount,
@@ -556,7 +556,7 @@ export class Render {
556
556
  }
557
557
 
558
558
  async screenshot({
559
- filename = this.sketch.name ?? this.sketch.key,
559
+ filename = this.sketch.key,
560
560
  pattern = this.sketch.filenamePattern,
561
561
  exportDir = this.sketch.exportDir,
562
562
  } = {}) {
@@ -67,10 +67,14 @@
67
67
  value(event);
68
68
  onclick(event);
69
69
  },
70
- download: (event) => {
71
- let [data, filename] = value(event);
70
+ download: async (event) => {
71
+ try {
72
+ let [data, filename] = await value(event);
72
73
 
73
- download(data, filename);
74
+ download(data, filename);
75
+ } catch (error) {
76
+ console.error(`Error while trying to download:`, error);
77
+ }
74
78
  },
75
79
  number: (event = {}) => {
76
80
  const isValueInRange = event.value >= 0 && event.value <= 1;
@@ -100,7 +100,7 @@
100
100
  transition: opacity 0.1s ease;
101
101
  }
102
102
 
103
- button.field__label {
103
+ button.field__label:not(:disabled) {
104
104
  cursor: pointer;
105
105
  }
106
106
 
@@ -9,7 +9,7 @@
9
9
 
10
10
  let gui = $derived(buildConfig.gui ?? {});
11
11
  let layout = $derived(buildConfig.layout ?? {});
12
- let headless = $derived(layout.headless ?? false);
12
+ let headless = $derived(layout.headless ?? true);
13
13
  let resizable = $derived(layout.resizable ?? false);
14
14
 
15
15
  let guiOutput = $derived(gui.output ?? true);
@@ -110,7 +110,11 @@
110
110
 
111
111
  let backgroundColor = $derived.by(() => {
112
112
  if (layout.previewing) {
113
- return sketch?.buildConfig?.backgroundColor ?? 'inherit';
113
+ return (
114
+ sketch?.buildConfig?.backgroundColor ??
115
+ sketch?.backgroundColor ??
116
+ 'inherit'
117
+ );
114
118
  }
115
119
 
116
120
  return sketch?.backgroundColor ?? 'inherit';
@@ -12,7 +12,7 @@
12
12
  };
13
13
  </script>
14
14
 
15
- <div class="checkbox">
15
+ <div class="checkbox" class:disabled>
16
16
  <input
17
17
  class="input"
18
18
  bind:checked={value}
@@ -20,7 +20,6 @@
20
20
  onchange={handleChange}
21
21
  disabled={disabled ? 'disabled' : null}
22
22
  />
23
- <div class="checked" />
24
23
  </div>
25
24
 
26
25
  <style>
@@ -36,11 +35,12 @@
36
35
  background-color: var(--fragment-input-background-color);
37
36
  }
38
37
 
39
- .checkbox:hover {
38
+ :global(body:not(.fragment-dragging)) .checkbox:not(.disabled):hover {
40
39
  box-shadow: inset 0 0 0 1px var(--fragment-accent-color);
41
40
  }
42
41
 
43
- .checkbox:focus-within {
42
+ :global(body:not(.fragment-dragging))
43
+ .checkbox:not(.disabled):focus-within {
44
44
  box-shadow: 0 0 0 2px var(--fragment-accent-color);
45
45
  }
46
46
 
@@ -53,7 +53,9 @@
53
53
  outline: 0;
54
54
  }
55
55
 
56
- .checked {
56
+ .checkbox:after {
57
+ content: '';
58
+
57
59
  position: absolute;
58
60
  left: 3px;
59
61
  top: 3px;
@@ -67,11 +69,11 @@
67
69
  pointer-events: none;
68
70
  }
69
71
 
70
- .input:checked + .checked {
72
+ .checkbox:has(.input:checked):after {
71
73
  opacity: 1;
72
74
  }
73
75
 
74
- .input:checked:disabled + .checked {
76
+ .checkbox:has(.input:checked:disabled):after {
75
77
  background-color: var(--fragment-color-disabled);
76
78
  }
77
79
  </style>