66 writeSharedPaletteHandoff
77} from "../shared/assetUsageIntegration.js" ;
88import { registerToolBootContract } from "../shared/toolBootContract.js" ;
9+ import { addToolModeMetadata , assertStandaloneToolDocument , offerImportMismatchOptions } from "../shared/documentModeGuards.js" ;
910
1011const CUSTOM_PALETTES_STORAGE_KEY = "toolboxaid.paletteBrowser.customPalettes" ;
1112const HIDDEN_BUILTIN_PALETTES_STORAGE_KEY = "toolboxaid.paletteBrowser.hiddenBuiltins" ;
@@ -31,6 +32,8 @@ const refs = {
3132 validationText : document . getElementById ( "paletteValidationText" ) ,
3233 selectionText : document . getElementById ( "paletteSelectionText" ) ,
3334 jsonPreview : document . getElementById ( "paletteJsonPreview" ) ,
35+ importPaletteJsonButton : document . getElementById ( "importPaletteJsonButton" ) ,
36+ importPaletteJsonInput : document . getElementById ( "importPaletteJsonInput" ) ,
3437 copyPaletteJsonButton : document . getElementById ( "copyPaletteJsonButton" ) ,
3538 exportPaletteJsonButton : document . getElementById ( "exportPaletteJsonButton" ) ,
3639 usePaletteButton : document . getElementById ( "usePaletteButton" )
@@ -44,6 +47,12 @@ const state = {
4447 hiddenBuiltInPaletteIds : loadHiddenBuiltInPaletteIds ( )
4548} ;
4649
50+ function setSelectionText ( text , options = { } ) {
51+ const muted = options ?. muted === true ;
52+ refs . selectionText . textContent = String ( text || "" ) ;
53+ refs . selectionText . classList . toggle ( "is-context-muted" , muted ) ;
54+ }
55+
4756function isWorkspaceContext ( ) {
4857 if ( typeof window === "undefined" ) {
4958 return false ;
@@ -317,6 +326,99 @@ function createCustomPalette(name, entries) {
317326 } ;
318327}
319328
329+ function makeUniquePaletteName ( baseName ) {
330+ const fallback = "imported-palette" ;
331+ const seed = String ( baseName || "" ) . trim ( ) || fallback ;
332+ const normalizedSeed = seed . toLowerCase ( ) ;
333+ const existing = new Set ( getAllPalettes ( ) . map ( ( palette ) => String ( palette . name || "" ) . trim ( ) . toLowerCase ( ) ) ) ;
334+ if ( ! existing . has ( normalizedSeed ) ) {
335+ return seed ;
336+ }
337+ let index = 2 ;
338+ while ( index < 1000 ) {
339+ const candidate = `${ seed } -${ index } ` ;
340+ if ( ! existing . has ( candidate . toLowerCase ( ) ) ) {
341+ return candidate ;
342+ }
343+ index += 1 ;
344+ }
345+ return `${ seed } -${ Date . now ( ) . toString ( 36 ) } ` ;
346+ }
347+
348+ function normalizeImportedPalette ( rawPayload ) {
349+ if ( ! rawPayload || typeof rawPayload !== "object" ) {
350+ throw new Error ( "Palette JSON must be an object." ) ;
351+ }
352+ const payload = rawPayload . palette && typeof rawPayload . palette === "object"
353+ ? rawPayload . palette
354+ : rawPayload ;
355+ const baseName = String ( payload . name || "" ) . trim ( ) ;
356+ const entries = Array . isArray ( payload . entries ) ? payload . entries : [ ] ;
357+ if ( ! entries . length ) {
358+ throw new Error ( "Palette JSON must include at least one swatch entry." ) ;
359+ }
360+ const normalizedEntries = entries . map ( ( entry , index ) => ( {
361+ symbol : String ( entry ?. symbol || "" ) . trim ( ) . slice ( 0 , 2 ) ,
362+ hex : String ( entry ?. hex || "" ) . trim ( ) ,
363+ name : String ( entry ?. name || `Swatch ${ index + 1 } ` ) . trim ( ) || `Swatch ${ index + 1 } `
364+ } ) ) ;
365+ return {
366+ name : baseName || "imported-palette" ,
367+ entries : normalizedEntries
368+ } ;
369+ }
370+
371+ async function importPaletteJsonFromFile ( file ) {
372+ if ( ! file ) {
373+ return ;
374+ }
375+ const text = await file . text ( ) ;
376+ const parsed = JSON . parse ( text ) ;
377+ const guard = assertStandaloneToolDocument ( parsed , {
378+ expectedLabel : "Palette Browser palette" ,
379+ requiredToolId : "palette-browser"
380+ } ) ;
381+ if ( ! guard . ok ) {
382+ const handled = offerImportMismatchOptions ( guard , {
383+ viewerToolId : "state-inspector" ,
384+ viewerPayload : parsed ,
385+ sourceToolId : "palette-browser"
386+ } ) ;
387+ if ( handled ) {
388+ return ;
389+ }
390+ throw new Error ( guard . reason ) ;
391+ }
392+ const imported = normalizeImportedPalette ( parsed ) ;
393+ let nextName = imported . name ;
394+ if ( hasReservedPaletteKeyword ( nextName ) ) {
395+ nextName = `${ nextName } -copy` ;
396+ }
397+ if ( hasReservedPaletteKeyword ( nextName ) ) {
398+ while ( true ) {
399+ const requested = window . prompt (
400+ "Imported palette name contains reserved terms. Enter a new name:" ,
401+ "imported-palette"
402+ ) ;
403+ if ( requested === null ) {
404+ setSelectionText ( "Palette import canceled." ) ;
405+ return ;
406+ }
407+ const trimmed = requested . trim ( ) || "imported-palette" ;
408+ if ( ! hasReservedPaletteKeyword ( trimmed ) ) {
409+ nextName = trimmed ;
410+ break ;
411+ }
412+ }
413+ }
414+ nextName = makeUniquePaletteName ( nextName ) ;
415+ const importedPalette = createCustomPalette ( nextName , imported . entries ) ;
416+ state . customPalettes . unshift ( importedPalette ) ;
417+ saveCustomPalettes ( ) ;
418+ setSelectedPalette ( importedPalette . id ) ;
419+ setSelectionText ( `Imported palette: ${ importedPalette . name } .` ) ;
420+ }
421+
320422function createNewPalette ( ) {
321423 const requestedName = window . prompt ( "Name for new palette:" , "new-palette" ) ;
322424 if ( requestedName === null ) {
@@ -440,9 +542,9 @@ async function copyPaletteJson() {
440542 } , null , 2 ) ;
441543 try {
442544 await navigator . clipboard . writeText ( payload ) ;
443- refs . selectionText . textContent = "Palette JSON copied to clipboard." ;
545+ setSelectionText ( "Palette JSON copied to clipboard." ) ;
444546 } catch {
445- refs . selectionText . textContent = "Clipboard copy unavailable in this environment." ;
547+ setSelectionText ( "Clipboard copy unavailable in this environment." ) ;
446548 }
447549}
448550
@@ -451,10 +553,10 @@ function exportPaletteJson() {
451553 if ( ! palette ) {
452554 return ;
453555 }
454- const payload = JSON . stringify ( {
556+ const payload = JSON . stringify ( addToolModeMetadata ( {
455557 name : palette . name ,
456558 entries : palette . entries
457- } , null , 2 ) ;
559+ } , { toolId : "palette-browser" } ) , null , 2 ) ;
458560 const blob = new Blob ( [ payload ] , { type : "application/json" } ) ;
459561 const objectUrl = URL . createObjectURL ( blob ) ;
460562 const link = document . createElement ( "a" ) ;
@@ -470,12 +572,12 @@ function usePaletteInActiveTools() {
470572 return ;
471573 }
472574 if ( ! isWorkspaceContext ( ) ) {
473- refs . selectionText . textContent = "Use in Workspace Manager is available only in Workspace Manager context." ;
575+ setSelectionText ( "Use in Workspace Manager is available only in Workspace Manager context." ) ;
474576 return ;
475577 }
476578 if ( hasReservedPaletteKeyword ( palette . name ) ) {
477579 const message = "Reserved palette names cannot be used for workspace shared palette. Duplicate and rename first." ;
478- refs . selectionText . textContent = message ;
580+ setSelectionText ( message ) ;
479581 window . alert ( message ) ;
480582 return ;
481583 }
@@ -486,7 +588,7 @@ function usePaletteInActiveTools() {
486588 && existingSharedPalette . paletteId !== palette . id
487589 ) {
488590 const message = `Shared palette is locked to ${ existingSharedPalette . displayName } . Edit swatches instead.` ;
489- refs . selectionText . textContent = message ;
591+ setSelectionText ( message ) ;
490592 window . alert ( message ) ;
491593 return ;
492594 }
@@ -496,7 +598,7 @@ function usePaletteInActiveTools() {
496598 && existingSharedPalette . paletteId === palette . id
497599 ) {
498600 const message = "Shared palette is locked. Edit swatches instead." ;
499- refs . selectionText . textContent = message ;
601+ setSelectionText ( message ) ;
500602 window . alert ( message ) ;
501603 return ;
502604 }
@@ -511,9 +613,9 @@ function usePaletteInActiveTools() {
511613 sourceToolId : context . sourceToolId || "palette-browser"
512614 } ) ;
513615 const stored = writeSharedPaletteHandoff ( handoff ) ;
514- refs . selectionText . textContent = stored
616+ setSelectionText ( stored
515617 ? `Shared palette handoff updated for ${ getToolDisplayName ( context . sourceToolId , "active tool" ) } : ${ palette . name } `
516- : "Shared palette handoff was not updated because the payload was invalid." ;
618+ : "Shared palette handoff was not updated because the payload was invalid." ) ;
517619}
518620
519621function deleteSelectedPalette ( ) {
@@ -543,12 +645,17 @@ function deleteSelectedPalette() {
543645}
544646
545647function renderStoredSelection ( ) {
648+ const workspaceMode = isWorkspaceContext ( ) ;
546649 const handoff = readSharedPaletteHandoff ( ) ;
547650 if ( ! handoff ) {
548- refs . selectionText . textContent = "No handoff recorded yet." ;
651+ setSelectionText ( workspaceMode
652+ ? "No handoff recorded yet."
653+ : "No workspace handoff recorded yet." , { muted : ! workspaceMode } ) ;
549654 return ;
550655 }
551- refs . selectionText . textContent = `Active handoff: ${ handoff . displayName } (${ handoff . selectedAt } )` ;
656+ setSelectionText ( workspaceMode
657+ ? `Active handoff: ${ handoff . displayName } (${ handoff . selectedAt } )`
658+ : `Last workspace handoff: ${ handoff . displayName } (${ handoff . selectedAt } )` , { muted : ! workspaceMode } ) ;
552659}
553660
554661function bindEvents ( ) {
@@ -584,6 +691,23 @@ function bindEvents() {
584691 refs . swatchColorInput . addEventListener ( "input" , updateSelectedSwatchFromInputs ) ;
585692 refs . swatchNameInput . addEventListener ( "input" , updateSelectedSwatchFromInputs ) ;
586693 refs . swatchSymbolInput . addEventListener ( "input" , updateSelectedSwatchFromInputs ) ;
694+ refs . importPaletteJsonButton . addEventListener ( "click" , ( ) => {
695+ refs . importPaletteJsonInput ?. click ( ) ;
696+ } ) ;
697+ refs . importPaletteJsonInput ?. addEventListener ( "change" , async ( ) => {
698+ const file = refs . importPaletteJsonInput . files ?. [ 0 ] ;
699+ refs . importPaletteJsonInput . value = "" ;
700+ if ( ! file ) {
701+ return ;
702+ }
703+ try {
704+ await importPaletteJsonFromFile ( file ) ;
705+ } catch ( error ) {
706+ const message = `Import failed: ${ error instanceof Error ? error . message : "invalid JSON" } ` ;
707+ setSelectionText ( message ) ;
708+ window . alert ( message ) ;
709+ }
710+ } ) ;
587711 refs . copyPaletteJsonButton . addEventListener ( "click" , copyPaletteJson ) ;
588712 refs . exportPaletteJsonButton . addEventListener ( "click" , exportPaletteJson ) ;
589713 refs . usePaletteButton . addEventListener ( "click" , usePaletteInActiveTools ) ;
0 commit comments