paperclip-theme 0.2.0 → 0.2.2

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.
package/src/ui/index.tsx CHANGED
@@ -25,6 +25,38 @@ const CARD_WIDTH = 192;
25
25
  const CARD_GAP = 10;
26
26
  const SWATCH_TOKENS = ["--background", "--primary", "--accent", "--chart-1", "--destructive"];
27
27
 
28
+ const FONT_OPTIONS_SANS = [
29
+ { label: "System Default", value: "-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" },
30
+ { label: "Inter", value: "'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif" },
31
+ { label: "Geist", value: "'Geist', -apple-system, BlinkMacSystemFont, sans-serif" },
32
+ { label: "Manrope", value: "'Manrope', -apple-system, BlinkMacSystemFont, sans-serif" },
33
+ { label: "Work Sans", value: "'Work Sans', -apple-system, BlinkMacSystemFont, sans-serif" },
34
+ { label: "DM Sans", value: "'DM Sans', -apple-system, BlinkMacSystemFont, sans-serif" },
35
+ { label: "Figtree", value: "'Figtree', -apple-system, BlinkMacSystemFont, sans-serif" },
36
+ { label: "Outfit", value: "'Outfit', -apple-system, BlinkMacSystemFont, sans-serif" },
37
+ { label: "Nunito", value: "'Nunito', -apple-system, BlinkMacSystemFont, sans-serif" },
38
+ { label: "Rubik", value: "'Rubik', -apple-system, BlinkMacSystemFont, sans-serif" },
39
+ { label: "Lato", value: "'Lato', -apple-system, BlinkMacSystemFont, sans-serif" },
40
+ { label: "IBM Plex Sans", value: "'IBM Plex Sans', -apple-system, BlinkMacSystemFont, sans-serif" },
41
+ { label: "Source Sans 3", value: "'Source Sans 3', 'Source Sans Pro', -apple-system, sans-serif" },
42
+ { label: "Plus Jakarta Sans", value: "'Plus Jakarta Sans', -apple-system, BlinkMacSystemFont, sans-serif" },
43
+ { label: "Noto Sans", value: "'Noto Sans', -apple-system, BlinkMacSystemFont, sans-serif" },
44
+ { label: "Custom", value: "__custom__" },
45
+ ];
46
+
47
+ const FONT_OPTIONS_MONO = [
48
+ { label: "System Default", value: "'SF Mono', Consolas, 'Liberation Mono', Menlo, monospace" },
49
+ { label: "JetBrains Mono", value: "'JetBrains Mono', 'Fira Code', Consolas, monospace" },
50
+ { label: "Fira Code", value: "'Fira Code', 'JetBrains Mono', Consolas, monospace" },
51
+ { label: "Cascadia Code", value: "'Cascadia Code', 'Fira Code', Consolas, monospace" },
52
+ { label: "Commit Mono", value: "'Commit Mono', 'JetBrains Mono', Consolas, monospace" },
53
+ { label: "IBM Plex Mono", value: "'IBM Plex Mono', Consolas, monospace" },
54
+ { label: "Source Code Pro", value: "'Source Code Pro', Consolas, monospace" },
55
+ { label: "Roboto Mono", value: "'Roboto Mono', Consolas, monospace" },
56
+ { label: "Geist Mono", value: "'Geist Mono', Consolas, monospace" },
57
+ { label: "Custom", value: "__custom__" },
58
+ ];
59
+
28
60
  /* ─── CSS injection engine ───────────────────────────────────────── */
29
61
 
30
62
  const STYLE_ELEMENT_ID = "blazo-theme-overrides";
@@ -385,6 +417,43 @@ const styles = {
385
417
  background: "var(--accent)",
386
418
  flexShrink: 0,
387
419
  }),
420
+ stickyBar: {
421
+ position: "sticky" as const,
422
+ top: 0,
423
+ zIndex: 50,
424
+ display: "flex",
425
+ alignItems: "center",
426
+ justifyContent: "space-between",
427
+ gap: 12,
428
+ padding: "10px 16px",
429
+ background: "var(--card)",
430
+ borderBottom: "1px solid var(--border)",
431
+ borderRadius: 8,
432
+ marginBottom: -8,
433
+ },
434
+ stickyThemeInfo: {
435
+ display: "flex",
436
+ flexDirection: "column" as const,
437
+ gap: 1,
438
+ minWidth: 0,
439
+ },
440
+ stickyThemeName: {
441
+ fontSize: 13,
442
+ fontWeight: 600,
443
+ color: "var(--foreground)",
444
+ whiteSpace: "nowrap" as const,
445
+ overflow: "hidden" as const,
446
+ textOverflow: "ellipsis" as const,
447
+ },
448
+ stickyStatus: {
449
+ fontSize: 11,
450
+ color: "var(--muted-foreground)",
451
+ },
452
+ stickyBtns: {
453
+ display: "flex",
454
+ gap: 8,
455
+ flexShrink: 0,
456
+ },
388
457
  actions: {
389
458
  display: "flex",
390
459
  gap: 10,
@@ -420,6 +489,41 @@ const styles = {
420
489
  alignSelf: "center" as const,
421
490
  transition: "opacity 300ms ease",
422
491
  },
492
+ fontRow: {
493
+ display: "flex",
494
+ flexDirection: "column" as const,
495
+ gap: 4,
496
+ padding: "6px 0",
497
+ },
498
+ fontLabel: {
499
+ fontSize: 13,
500
+ fontWeight: 400,
501
+ color: "var(--foreground)",
502
+ },
503
+ fontSelect: {
504
+ padding: "7px 10px",
505
+ borderRadius: 6,
506
+ border: "1.5px solid var(--border)",
507
+ background: "var(--muted)",
508
+ color: "var(--foreground)",
509
+ fontSize: 13,
510
+ cursor: "pointer",
511
+ outline: "none",
512
+ width: "100%",
513
+ },
514
+ fontCustomInput: {
515
+ marginTop: 6,
516
+ padding: "7px 10px",
517
+ borderRadius: 6,
518
+ border: "1.5px solid var(--border)",
519
+ background: "var(--muted)",
520
+ color: "var(--foreground)",
521
+ fontSize: 12,
522
+ fontFamily: "var(--font-mono, monospace)",
523
+ outline: "none",
524
+ width: "100%",
525
+ boxSizing: "border-box" as const,
526
+ },
423
527
  overlay: {
424
528
  position: "fixed" as const,
425
529
  inset: 0,
@@ -683,6 +787,69 @@ function TokenRow({
683
787
  );
684
788
  }
685
789
 
790
+ /* ─── Font Row ───────────────────────────────────────────────────── */
791
+
792
+ function FontRow({
793
+ label,
794
+ token,
795
+ value,
796
+ options,
797
+ onChange,
798
+ }: {
799
+ label: string;
800
+ token: string;
801
+ value: string;
802
+ options: { label: string; value: string }[];
803
+ onChange: (token: string, value: string) => void;
804
+ }) {
805
+ const knownOption = options.find((o) => o.value !== "__custom__" && o.value === value);
806
+ const isCustom = !knownOption && value !== "";
807
+ const selectValue = isCustom ? "__custom__" : (value || options[0]!.value);
808
+ const [showCustom, setShowCustom] = useState(isCustom);
809
+
810
+ return (
811
+ <div style={styles.fontRow}>
812
+ <span style={styles.fontLabel}>{label}</span>
813
+ <select
814
+ style={styles.fontSelect}
815
+ value={selectValue}
816
+ onChange={(e) => {
817
+ if (e.target.value === "__custom__") {
818
+ setShowCustom(true);
819
+ } else {
820
+ setShowCustom(false);
821
+ onChange(token, e.target.value);
822
+ }
823
+ }}
824
+ >
825
+ {options.map((o) => (
826
+ <option key={o.value} value={o.value}>
827
+ {o.label}
828
+ </option>
829
+ ))}
830
+ </select>
831
+ {showCustom && (
832
+ <input
833
+ type="text"
834
+ placeholder="e.g. 'Roboto', sans-serif"
835
+ defaultValue={isCustom ? value : ""}
836
+ style={styles.fontCustomInput}
837
+ onBlur={(e) => {
838
+ const v = e.target.value.trim();
839
+ if (v) onChange(token, v);
840
+ }}
841
+ onKeyDown={(e) => {
842
+ if (e.key === "Enter") {
843
+ const v = (e.target as HTMLInputElement).value.trim();
844
+ if (v) onChange(token, v);
845
+ }
846
+ }}
847
+ />
848
+ )}
849
+ </div>
850
+ );
851
+ }
852
+
686
853
  /* ─── Main Settings Page Component ───────────────────────────────── */
687
854
 
688
855
  export function ThemeSettingsPage() {
@@ -840,6 +1007,52 @@ export function ThemeSettingsPage() {
840
1007
 
841
1008
  return (
842
1009
  <div style={styles.root}>
1010
+ {/* Sticky Save Bar */}
1011
+ <div style={styles.stickyBar}>
1012
+ <div style={styles.stickyThemeInfo}>
1013
+ <span style={styles.stickyThemeName}>
1014
+ {localTheme ? localTheme.name : "No theme selected"}
1015
+ </span>
1016
+ <span style={styles.stickyStatus}>
1017
+ {saving
1018
+ ? "Saving\u2026"
1019
+ : savedAt
1020
+ ? `Saved at ${savedAt}`
1021
+ : hasUnsaved
1022
+ ? "Unsaved changes"
1023
+ : "Saved"}
1024
+ </span>
1025
+ </div>
1026
+ <div style={styles.stickyBtns}>
1027
+ <button
1028
+ type="button"
1029
+ style={{
1030
+ ...styles.btnSecondary,
1031
+ padding: "7px 14px",
1032
+ fontSize: 12,
1033
+ }}
1034
+ onClick={handleReset}
1035
+ disabled={saving}
1036
+ >
1037
+ Reset
1038
+ </button>
1039
+ <button
1040
+ type="button"
1041
+ style={{
1042
+ ...styles.btnPrimary,
1043
+ padding: "7px 14px",
1044
+ fontSize: 12,
1045
+ opacity: saving || !hasUnsaved ? 0.45 : 1,
1046
+ pointerEvents: saving || !hasUnsaved ? "none" : "auto",
1047
+ }}
1048
+ onClick={handleSave}
1049
+ disabled={saving || !hasUnsaved}
1050
+ >
1051
+ Save Theme
1052
+ </button>
1053
+ </div>
1054
+ </div>
1055
+
843
1056
  {/* Header */}
844
1057
  <div style={styles.header}>
845
1058
  <h2 style={styles.title}>Theme</h2>
@@ -911,6 +1124,30 @@ export function ThemeSettingsPage() {
911
1124
  </div>
912
1125
  )}
913
1126
 
1127
+ {/* Typography */}
1128
+ {localTheme && (
1129
+ <div style={styles.section}>
1130
+ <p style={styles.sectionLabel}>Typography</p>
1131
+ <p style={{ ...styles.subtitle, marginTop: -8 }}>
1132
+ Choose fonts for the UI. Select a preset or enter a custom CSS font stack.
1133
+ </p>
1134
+ <FontRow
1135
+ label="Sans-serif (UI font)"
1136
+ token="--font-sans"
1137
+ value={localTheme.tokens["--font-sans"] ?? ""}
1138
+ options={FONT_OPTIONS_SANS}
1139
+ onChange={updateToken}
1140
+ />
1141
+ <FontRow
1142
+ label="Monospace (code font)"
1143
+ token="--font-mono"
1144
+ value={localTheme.tokens["--font-mono"] ?? ""}
1145
+ options={FONT_OPTIONS_MONO}
1146
+ onChange={updateToken}
1147
+ />
1148
+ </div>
1149
+ )}
1150
+
914
1151
  {/* Radius */}
915
1152
  {localTheme && (
916
1153
  <div style={styles.section}>
@@ -931,36 +1168,6 @@ export function ThemeSettingsPage() {
931
1168
  </div>
932
1169
  )}
933
1170
 
934
- {/* Actions */}
935
- <div style={styles.actions}>
936
- <button
937
- type="button"
938
- style={{
939
- ...styles.btnPrimary,
940
- opacity: saving || !hasUnsaved ? 0.5 : 1,
941
- pointerEvents: saving || !hasUnsaved ? "none" : "auto",
942
- }}
943
- onClick={handleSave}
944
- disabled={saving || !hasUnsaved}
945
- >
946
- {saving ? "Saving\u2026" : "Save Theme"}
947
- </button>
948
- <button
949
- type="button"
950
- style={styles.btnSecondary}
951
- onClick={handleReset}
952
- disabled={saving}
953
- >
954
- Reset to Default
955
- </button>
956
- {savedAt && (
957
- <span style={styles.saved}>Saved at {savedAt}</span>
958
- )}
959
- {hasUnsaved && !savedAt && (
960
- <span style={{ ...styles.saved, color: "var(--chart-1)" }}>Unsaved changes</span>
961
- )}
962
- </div>
963
-
964
1171
  {/* Import / Export */}
965
1172
  <div style={styles.section}>
966
1173
  <p style={styles.sectionLabel}>Share</p>