Plugin Directory

Changeset 3783861


Ignore:
Timestamp:
11/04/2026 12:01:29 PM (38 hours ago)
Author:
awoscan
Message:

Release AwoSign 1.3.0 with multi-template signatures, [awosign] shortcode, screenshots, rich banner, and Playground blueprint

Location:
awosign
Files:
17 added
7 edited

Legend:

Unmodified
Added
Removed
  • awosign/trunk/assets/designer.css

    r3783841 r3783861  
     1/* AwoSign v1.3.0 — GPL-2.0-or-later */
    12#awosign-root {
    2   font-family:Arial,Helvetica,sans-serif;
    3   color-scheme:dark;
    4   --brand:#172e49;
    5   --accent:#e7b52e
    6 }
     3  --brand: #172e49;
     4  --accent: #e7b52e;
     5  --as-bg: #09131f;
     6  --as-panel: #182a3e;
     7  --as-panel-border: #304963;
     8  --as-stage: #101e2e;
     9  --as-stage-border: #4c6680;
     10  --as-input-bg: #102032;
     11  --as-input-border: #47617a;
     12  --as-text: #eaf2fc;
     13  --as-muted: #91abc5;
     14  --as-hint: #b9cee3;
     15  --as-header-bg: #172b43;
     16  font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Arial, sans-serif;
     17  color-scheme: dark;
     18  margin: 20px 20px 20px 0;
     19  min-height: 80vh;
     20  background: var(--as-bg);
     21  color: var(--as-text);
     22  border-radius: 14px;
     23  overflow: hidden;
     24  transition: background-color 0.2s ease, color 0.2s ease;
     25}
     26
     27/* Light Mode: Explicit or Automatic via System/Page */
     28#awosign-root[data-theme="light"],
     29html:not(.dark):not([data-theme="dark"]) body:not(.dark):not(.dark-mode):not(.wp-dark-mode-active) #awosign-root[data-theme="auto"] {
     30  --as-bg: #edf2f8;
     31  --as-panel: #ffffff;
     32  --as-panel-border: #d2dfed;
     33  --as-stage: #f3f6fa;
     34  --as-stage-border: #b8c9dc;
     35  --as-input-bg: #ffffff;
     36  --as-input-border: #bdcad9;
     37  --as-text: #172e49;
     38  --as-muted: #526477;
     39  --as-hint: #3b536e;
     40  --as-header-bg: #102438;
     41  color-scheme: light;
     42}
     43
     44@media (prefers-color-scheme: dark) {
     45  #awosign-root[data-theme="auto"] {
     46    --as-bg: #09131f;
     47    --as-panel: #182a3e;
     48    --as-panel-border: #304963;
     49    --as-stage: #101e2e;
     50    --as-stage-border: #4c6680;
     51    --as-input-bg: #102032;
     52    --as-input-border: #47617a;
     53    --as-text: #eaf2fc;
     54    --as-muted: #91abc5;
     55    --as-hint: #b9cee3;
     56    --as-header-bg: #172b43;
     57    color-scheme: dark;
     58  }
     59}
     60
    761#awosign-root * {
    8   box-sizing:border-box
    9 }
    10 #awosign-root {
    11   margin:0;
    12   background:#09131f;
    13   color:#eaf2fc
    14 }
     62  box-sizing: border-box;
     63}
     64
    1565#awosign-root header {
    16   background:#172b43;
    17   padding:18px 26px;
    18   display:flex;
    19   align-items:center;
    20   justify-content:space-between;
    21   gap:16px;
    22   flex-wrap:wrap
    23 }
     66  background: var(--as-header-bg);
     67  color: #ffffff;
     68  padding: 18px 26px;
     69  display: flex;
     70  align-items: center;
     71  justify-content: space-between;
     72  gap: 16px;
     73  flex-wrap: wrap;
     74}
     75
    2476#awosign-root header h1 {
    25   margin:0;
    26   font-size:25px
    27 }
     77  margin: 0;
     78  font-size: 24px;
     79  color: #ffffff;
     80}
     81
    2882#awosign-root header small {
    29   color:#b8c8dc
    30 }
     83  color: #b8c8dc;
     84  display: block;
     85  margin-top: 4px;
     86}
     87
     88#awosign-root header label {
     89  color: #ffffff;
     90  margin-top: 0;
     91}
     92
     93#awosign-root header select {
     94  background: #102032;
     95  color: #ffffff;
     96  border-color: #47617a;
     97}
     98
    3199#awosign-root main {
    32   padding:22px;
    33   display:grid;
    34   grid-template-columns:minmax(290px,390px) minmax(0,1fr);
    35   gap:20px
    36 }
     100  padding: 22px;
     101  display: grid;
     102  grid-template-columns: minmax(300px, 400px) minmax(0, 1fr);
     103  gap: 20px;
     104  align-items: start;
     105}
     106
    37107#awosign-root .panel {
    38   background:#182a3e;
    39   border:1px solid #304963;
    40   border-radius:14px;
    41   padding:22px
    42 }
     108  background: var(--as-panel);
     109  border: 1px solid var(--as-panel-border);
     110  border-radius: 14px;
     111  padding: 22px;
     112}
     113
    43114#awosign-root h2 {
    44   font-size:18px;
    45   margin:0 0 17px
    46 }
     115  font-size: 18px;
     116  margin: 0 0 14px;
     117  color: inherit;
     118}
     119
    47120#awosign-root h3 {
    48   font-size:14px;
    49   margin:18px 0 9px
    50 }
     121  font-size: 14px;
     122  margin: 16px 0 9px;
     123  color: inherit;
     124}
     125
    51126#awosign-root label {
    52   display:block;
    53   font-size:12px;
    54   font-weight:bold;
    55   margin:12px 0 5px
    56 }
    57 #awosign-root input,#awosign-root select,#awosign-root textarea {
    58   width:100%;
    59   border:1px solid #47617a;
    60   border-radius:7px;
    61   background:#102032;
    62   color:#fff;
    63   padding:10px;
    64   font:inherit;
    65   font-size:13px
    66 }
    67 #awosign-root input[type=color] {
    68   height:40px;
    69   padding:3px
    70 }
    71 #awosign-root input[type=checkbox] {
    72   width:auto;
    73   accent-color:#e7b52e
    74 }
    75 #awosign-root input[type=file] {
    76   font-size:12px
    77 }
     127  display: block;
     128  font-size: 12.5px;
     129  font-weight: 600;
     130  margin: 11px 0 5px;
     131}
     132
     133#awosign-root .as-inline-toggle {
     134  display: flex;
     135  align-items: center;
     136  gap: 8px;
     137  cursor: pointer;
     138  margin: 10px 0;
     139}
     140
     141#awosign-root input,
     142#awosign-root select,
     143#awosign-root textarea {
     144  width: 100%;
     145  border: 1px solid var(--as-input-border);
     146  border-radius: 7px;
     147  background: var(--as-input-bg);
     148  color: var(--as-text);
     149  padding: 9px 10px;
     150  font: inherit;
     151  font-size: 13px;
     152}
     153
     154#awosign-root input[type="color"] {
     155  height: 40px;
     156  padding: 3px;
     157  cursor: pointer;
     158}
     159
     160#awosign-root input[type="checkbox"] {
     161  width: auto;
     162  accent-color: #e7b52e;
     163}
     164
     165#awosign-root input[type="file"] {
     166  font-size: 12px;
     167}
     168
    78169#awosign-root .two {
    79   display:grid;
    80   grid-template-columns:1fr 1fr;
    81   gap:12px
    82 }
     170  display: grid;
     171  grid-template-columns: 1fr 1fr;
     172  gap: 12px;
     173}
     174
    83175#awosign-root .social {
    84   display:grid;
    85   grid-template-columns:repeat(2,minmax(0,1fr));
    86   gap:8px
    87 }
     176  display: grid;
     177  grid-template-columns: repeat(2, minmax(0, 1fr));
     178  gap: 8px;
     179}
     180
    88181#awosign-root .social label {
    89   display:flex;
    90   align-items:center;
    91   gap:7px;
    92   font-size:12px;
    93   margin:0;
    94   background:#102032;
    95   border-radius:7px;
    96   padding:9px
    97 }
     182  display: flex;
     183  align-items: center;
     184  gap: 7px;
     185  font-size: 12px;
     186  margin: 0;
     187  background: var(--as-input-bg);
     188  border: 1px solid var(--as-input-border);
     189  border-radius: 7px;
     190  padding: 8px 10px;
     191  cursor: pointer;
     192}
     193
    98194#awosign-root .tabs {
    99   display:flex;
    100   flex-wrap:wrap;
    101   gap:8px;
    102   margin-bottom:18px
    103 }
     195  display: flex;
     196  flex-wrap: wrap;
     197  gap: 8px;
     198  margin-bottom: 18px;
     199}
     200
    104201#awosign-root button {
    105   border:1px solid #54708e;
    106   background:#25486d;
    107   color:white;
    108   padding:10px 13px;
    109   border-radius:8px;
    110   cursor:pointer;
    111   font-weight:bold
    112 }
     202  border: 1px solid #415e7c;
     203  background: #1d3b5a;
     204  color: #ffffff;
     205  padding: 9px 14px;
     206  border-radius: 8px;
     207  cursor: pointer;
     208  font-weight: 600;
     209  font-size: 13px;
     210  transition: background 0.15s ease;
     211}
     212
     213#awosign-root button:hover {
     214  background: #2b527a;
     215}
     216
     217#awosign-root button.as-btn-primary {
     218  background: #1a633f;
     219  border-color: #237d50;
     220}
     221
     222#awosign-root button.as-btn-primary:hover {
     223  background: #237d50;
     224}
     225
     226#awosign-root button.as-btn-accent {
     227  background: #9a6f15;
     228  border-color: #b5831a;
     229}
     230
     231#awosign-root button.as-btn-accent:hover {
     232  background: #b5831a;
     233}
     234
    113235#awosign-root button.active {
    114   background:#e7b52e;
    115   color:#102032;
    116   border-color:#e7b52e
    117 }
     236  background: #e7b52e;
     237  color: #102032;
     238  border-color: #e7b52e;
     239}
     240
     241#awosign-root button:focus-visible,
     242#awosign-root input:focus-visible,
     243#awosign-root select:focus-visible,
     244#awosign-root textarea:focus-visible {
     245  outline: 3px solid #e7b52e;
     246  outline-offset: 2px;
     247}
     248
    118249#awosign-root .stage {
    119   background:#101e2e;
    120   border:1px dashed #4c6680;
    121   border-radius:12px;
    122   padding:24px;
    123   overflow:auto
    124 }
     250  background: var(--as-stage);
     251  border: 1px dashed var(--as-stage-border);
     252  border-radius: 12px;
     253  padding: 24px;
     254  overflow: auto;
     255}
     256
    125257#awosign-root .stage h3 {
    126   margin:0 0 13px;
    127   color:#c8d9ee
    128 }
     258  margin: 0 0 13px;
     259}
     260
    129261#awosign-root .signature {
    130   background:white;
    131   color:#15263c;
    132   width:600px;
    133   max-width:100%;
    134   padding:18px;
    135   border-radius:2px;
    136   font-family:Arial,sans-serif
    137 }
     262  background: #ffffff;
     263  color: #15263c;
     264  width: 600px;
     265  max-width: 100%;
     266  padding: 18px;
     267  border-radius: 4px;
     268  font-family: Arial, Helvetica, sans-serif;
     269  box-shadow: 0 4px 18px rgba(0, 0, 0, 0.12);
     270}
     271
    138272#awosign-root .sigrow {
    139   display:flex;
    140   align-items:center;
    141   gap:20px
    142 }
     273  display: flex;
     274  align-items: center;
     275  gap: 20px;
     276}
     277
    143278#awosign-root .siglogo {
    144   width:170px;
    145   min-width:170px;
    146   text-align:center
    147 }
     279  width: 170px;
     280  min-width: 170px;
     281  text-align: center;
     282}
     283
    148284#awosign-root .siglogo img {
    149   max-width:100%;
    150   max-height:110px;
    151   object-fit:contain
    152 }
     285  max-width: 100%;
     286  max-height: 110px;
     287  object-fit: contain;
     288}
     289
     290#awosign-root .siglogo div {
     291  overflow-wrap: anywhere;
     292}
     293
    153294#awosign-root .siginfo {
    154   border-left:2px solid var(--accent);
    155   padding-left:17px;
    156   line-height:1.55;
    157   font-size:12px
    158 }
     295  border-left: 2px solid var(--accent);
     296  padding-left: 17px;
     297  line-height: 1.55;
     298  font-size: 12px;
     299}
     300
    159301#awosign-root .siginfo strong {
    160   font-size:19px
    161 }
     302  font-size: 19px;
     303}
     304
    162305#awosign-root .sigaddress {
    163   font-size:11px;
    164   margin-top:12px;
    165   border-top:1px solid #ddd;
    166   padding-top:9px
    167 }
     306  font-size: 11px;
     307  margin-top: 12px;
     308  border-top: 1px solid #dddddd;
     309  padding-top: 9px;
     310}
     311
     312/* 85 x 55 mm Exact Business Cards (Front & Back) */
    168313#awosign-root .cards {
    169   display:flex;
    170   gap:20px;
    171   flex-wrap:wrap
    172 }
     314  display: flex;
     315  gap: 24px;
     316  flex-wrap: wrap;
     317  align-items: flex-start;
     318}
     319
    173320#awosign-root .cardwrap {
    174   width:min(100%,460px)
    175 }
    176 #awosign-root .card {
    177   aspect-ratio:85/55;
    178   border-radius:9px;
    179   overflow:hidden;
    180   padding:24px;
    181   position:relative
    182 }
    183 #awosign-root .front {
    184   background:var(--brand);
    185   color:white;
    186   display:flex;
    187   flex-direction:column;
    188   align-items:center;
    189   justify-content:center;
    190   text-align:center;
    191   border-top:5px solid var(--accent)
    192 }
    193 #awosign-root .front img {
    194   max-width:55%;
    195   max-height:95px;
    196   object-fit:contain
    197 }
    198 #awosign-root .front strong {
    199   font-size:22px;
    200   margin-top:12px
    201 }
    202 #awosign-root .back {
    203   background:white;
    204   color:#15263c;
    205   border-left:6px solid var(--accent);
    206   display:flex;
    207   align-items:center;
    208   justify-content:space-between;
    209   gap:12px
    210 }
    211 #awosign-root .back .details {
    212   min-width:0;
    213   overflow-wrap:anywhere;
    214   font-size:12px;
    215   line-height:1.6
    216 }
    217 #awosign-root .back strong {
    218   font-size:20px
    219 }
    220 #awosign-root .qr {
    221   background:white;
    222   padding:5px;
    223   border-radius:5px;
    224   width:105px;
    225   height:105px;
    226   object-fit:contain;
    227   flex-shrink:0
    228 }
    229 #awosign-root .socialicons {
    230   display:flex;
    231   gap:5px;
    232   flex-wrap:wrap;
    233   margin-top:8px
    234 }
    235 #awosign-root .socialicons span {
    236   background:var(--brand);
    237   color:#fff;
    238   font-size:10px;
    239   padding:5px 7px;
    240   border-radius:4px;
    241   font-weight:bold
    242 }
     321  display: flex;
     322  flex-direction: column;
     323  gap: 8px;
     324}
     325
     326#awosign-root .card,
     327#as-print-host .card {
     328  width: 85mm;
     329  height: 55mm;
     330  box-sizing: border-box;
     331  border-radius: 0;
     332  overflow: hidden;
     333  padding: 5.5mm;
     334  position: relative;
     335  font-family: Arial, Helvetica, sans-serif;
     336  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.18);
     337}
     338
     339#awosign-root .card.show-crop-guide {
     340  outline: 1px dashed #7f95ab;
     341  outline-offset: -0.2mm;
     342}
     343
     344#awosign-root .front,
     345#as-print-host .front {
     346  background: var(--brand, #172e49);
     347  color: #ffffff;
     348  display: flex;
     349  flex-direction: column;
     350  align-items: center;
     351  justify-content: center;
     352  text-align: center;
     353  border-top: 1.5mm solid var(--accent, #e7b52e);
     354}
     355
     356#awosign-root .front img,
     357#as-print-host .front img {
     358  max-width: 46mm;
     359  max-height: 22mm;
     360  object-fit: contain;
     361}
     362
     363#awosign-root .front strong,
     364#as-print-host .front strong {
     365  font-size: 13pt;
     366  margin-top: 2.5mm;
     367}
     368
     369#awosign-root .front small,
     370#as-print-host .front small {
     371  font-size: 7.5pt;
     372  color: #dbe7f5;
     373  margin-top: 1mm;
     374}
     375
     376#awosign-root .back,
     377#as-print-host .back {
     378  background: #ffffff;
     379  color: #15263c;
     380  border-left: 1.8mm solid var(--accent, #e7b52e);
     381  display: flex;
     382  align-items: center;
     383  justify-content: space-between;
     384  gap: 3mm;
     385}
     386
     387#awosign-root .back .details,
     388#as-print-host .back .details {
     389  min-width: 0;
     390  overflow-wrap: anywhere;
     391  font-size: 7.4pt;
     392  line-height: 1.38;
     393}
     394
     395#awosign-root .back strong,
     396#as-print-host .back strong {
     397  font-size: 11.5pt;
     398  color: #102032;
     399}
     400
     401#awosign-root .qr,
     402#as-print-host .qr {
     403  background: #ffffff;
     404  padding: 1mm;
     405  border-radius: 1mm;
     406  border: 0.2mm solid #d8e0e8;
     407  width: 22mm;
     408  height: 22mm;
     409  object-fit: contain;
     410  flex-shrink: 0;
     411}
     412
     413#awosign-root .socialicons,
     414#as-print-host .socialicons {
     415  display: flex;
     416  gap: 1.2mm;
     417  flex-wrap: wrap;
     418  margin-top: 1.8mm;
     419}
     420
     421#awosign-root .socialicons span,
     422#as-print-host .socialicons span {
     423  background: var(--brand, #172e49);
     424  color: #ffffff;
     425  font-size: 6.5pt;
     426  padding: 0.8mm 1.5mm;
     427  border-radius: 0.8mm;
     428  font-weight: bold;
     429  display: inline-block;
     430}
     431
    243432#awosign-root .digital {
    244   width:540px;
    245   max-width:100%;
    246   background:var(--brand);
    247   color:white;
    248   border-top:5px solid var(--accent);
    249   padding:24px;
    250   border-radius:12px;
    251   display:flex;
    252   justify-content:space-between;
    253   align-items:center;
    254   gap:12px;
    255   min-height:310px
    256 }
     433  width: 540px;
     434  max-width: 100%;
     435  background: var(--brand);
     436  color: #ffffff;
     437  border-top: 5px solid var(--accent);
     438  padding: 24px;
     439  border-radius: 12px;
     440  display: flex;
     441  justify-content: space-between;
     442  align-items: center;
     443  gap: 16px;
     444  min-height: 290px;
     445  box-shadow: 0 6px 24px rgba(0, 0, 0, 0.2);
     446}
     447
    257448#awosign-root .digital img.logo {
    258   width:100px;
    259   max-height:70px;
    260   object-fit:contain
    261 }
     449  width: 110px;
     450  max-height: 70px;
     451  object-fit: contain;
     452}
     453
    262454#awosign-root .digital strong {
    263   font-size:22px
    264 }
     455  font-size: 22px;
     456}
     457
    265458#awosign-root .digital .details {
    266   line-height:1.7;
    267   font-size:13px;
    268   overflow-wrap:anywhere
    269 }
     459  line-height: 1.65;
     460  font-size: 13px;
     461  overflow-wrap: anywhere;
     462}
     463
     464#awosign-root .digital .qr {
     465  width: 108px;
     466  height: 108px;
     467}
     468
     469/* Print & Bulk Duplication Panel */
     470#awosign-root .as-staff-bar {
     471  background: var(--as-input-bg);
     472  border: 1px solid var(--as-input-border);
     473  border-radius: 10px;
     474  padding: 14px;
     475  margin-bottom: 14px;
     476}
     477
     478#awosign-root .as-staff-header {
     479  display: flex;
     480  align-items: center;
     481  justify-content: space-between;
     482  gap: 8px;
     483  margin-bottom: 6px;
     484}
     485
     486#awosign-root .as-staff-header h2 {
     487  margin: 0;
     488  font-size: 15px;
     489}
     490
     491#awosign-root .as-badge {
     492  background: #e7b52e;
     493  color: #102032;
     494  font-size: 11px;
     495  font-weight: 700;
     496  padding: 3px 9px;
     497  border-radius: 20px;
     498}
     499
     500#awosign-root .as-shortcode-box {
     501  margin-top: 10px;
     502  padding-top: 10px;
     503  border-top: 1px dashed var(--as-input-border);
     504}
     505
     506#awosign-root .as-shortcode-row,
     507#awosign-root .as-logo-row {
     508  display: flex;
     509  gap: 8px;
     510  align-items: center;
     511}
     512
     513#awosign-root .as-shortcode-row input {
     514  font-family: monospace;
     515  font-size: 12px;
     516}
     517
     518#awosign-root .as-logo-row button,
     519#awosign-root .as-shortcode-row button {
     520  white-space: nowrap;
     521  flex-shrink: 0;
     522}
     523
     524#awosign-root .as-file-btn {
     525  display: inline-flex;
     526  align-items: center;
     527  border: 1px solid #415e7c;
     528  background: #1d3b5a;
     529  color: #ffffff;
     530  padding: 9px 14px;
     531  border-radius: 8px;
     532  cursor: pointer;
     533  font-weight: 600;
     534  font-size: 13px;
     535  margin: 0;
     536}
     537
     538#awosign-root .as-print-panel {
     539  background: var(--as-input-bg);
     540  border: 1px solid var(--as-input-border);
     541  border-radius: 10px;
     542  padding: 14px 16px;
     543  margin-top: 16px;
     544}
     545
     546#awosign-root .as-print-panel h4 {
     547  margin: 0 0 10px;
     548  font-size: 14px;
     549}
     550
     551#awosign-root .as-print-grid {
     552  display: grid;
     553  grid-template-columns: minmax(220px, 1.6fr) minmax(130px, 1fr);
     554  gap: 12px;
     555  align-items: end;
     556}
     557
    270558#awosign-root .hint {
    271   font-size:12px;
    272   color:#b9cee3;
    273   margin-top:12px
    274 }
     559  font-size: 12px;
     560  color: var(--as-hint);
     561  margin-top: 10px;
     562}
     563
    275564#awosign-root .muted {
    276   color:#91abc5;
    277   font-size:12px
    278 }
     565  color: var(--as-muted);
     566  font-size: 12px;
     567  margin-top: 14px;
     568}
     569
     570#awosign-root #as-status {
     571  font-size: 13px;
     572  font-weight: 600;
     573  min-height: 18px;
     574  margin-top: 10px;
     575  color: #e7b52e;
     576}
     577
    279578#awosign-root .hidden {
    280   display:none!important
    281 }
     579  display: none !important;
     580}
     581
    282582#awosign-root .buttons {
    283   display:flex;
    284   gap:8px;
    285   flex-wrap:wrap;
    286   margin-top:15px
    287 }
     583  display: flex;
     584  gap: 8px;
     585  flex-wrap: wrap;
     586  margin-top: 15px;
     587}
     588
    288589#awosign-root a {
    289   color:inherit
    290 }
    291 #awosign-root.light {
    292   background:#edf2f8;
    293   color:#172e49;
    294   color-scheme:light
    295 }
    296 #awosign-root.light .panel {
    297   background:white;
    298   border-color:#d2dfed
    299 }
    300 #awosign-root.light .stage {
    301   background:#f3f6fa
    302 }
    303 #awosign-root.light input,#awosign-root.light select,#awosign-root.light textarea {
    304   background:white;
    305   color:#172e49;
    306   border-color:#bdcad9
    307 }
    308 #awosign-root.light .social label {
    309   background:#edf2f8
    310 }
    311 #awosign-root.light .stage h3 {
    312   color:#243f5d
    313 }
    314 @media(max-width:950px) {
     590  color: inherit;
     591}
     592
     593#awosign-root .privacy {
     594  padding: 12px 22px;
     595  background: #102032;
     596  color: #d7e4f5;
     597  margin: 0;
     598  font-size: 12.5px;
     599}
     600
     601/* In-Page Print Host (Replaces popup window.open) */
     602#as-print-host {
     603  display: none;
     604}
     605
     606#as-print-host .as-a4-sheet {
     607  width: 210mm;
     608  height: 297mm;
     609  box-sizing: border-box;
     610  padding: 11mm 20mm;
     611  background: #ffffff;
     612  display: grid;
     613  grid-template-columns: repeat(2, 85mm);
     614  grid-template-rows: repeat(5, 55mm);
     615  gap: 0;
     616  break-after: page;
     617  page-break-after: always;
     618  -webkit-print-color-adjust: exact;
     619  print-color-adjust: exact;
     620}
     621
     622#as-print-host .as-a4-sheet:last-child {
     623  break-after: auto;
     624  page-break-after: auto;
     625}
     626
     627#as-print-host .as-a4-cell {
     628  width: 85mm;
     629  height: 55mm;
     630  position: relative;
     631  box-sizing: border-box;
     632}
     633
     634#as-print-host .as-a4-cell.has-crop-lines {
     635  outline: 0.15mm dashed #9aa8b6;
     636}
     637
     638#as-print-host .as-bleed-sheet {
     639  width: 97mm;
     640  height: 67mm;
     641  padding: 6mm;
     642  box-sizing: border-box;
     643  background: #ffffff;
     644  position: relative;
     645  break-after: page;
     646  page-break-after: always;
     647  -webkit-print-color-adjust: exact;
     648  print-color-adjust: exact;
     649}
     650
     651#as-print-host .as-bleed-sheet:last-child {
     652  break-after: auto;
     653  page-break-after: auto;
     654}
     655
     656#as-print-host .as-bleed-sheet .card {
     657  position: relative;
     658  z-index: 2;
     659  box-shadow: none !important;
     660}
     661
     662#as-print-host .as-bleed-bg-front {
     663  position: absolute;
     664  top: 3mm;
     665  left: 3mm;
     666  width: 91mm;
     667  height: 61mm;
     668  background: var(--brand, #172e49);
     669  border-top: 4.5mm solid var(--accent, #e7b52e);
     670  z-index: 1;
     671}
     672
     673#as-print-host .as-bleed-bg-back {
     674  position: absolute;
     675  top: 3mm;
     676  left: 3mm;
     677  width: 91mm;
     678  height: 61mm;
     679  background: #ffffff;
     680  border-left: 4.8mm solid var(--accent, #e7b52e);
     681  z-index: 1;
     682}
     683
     684#as-print-host .as-crop-mark {
     685  position: absolute;
     686  background: #000000;
     687  z-index: 3;
     688}
     689
     690#as-print-host .as-crop-tl-h { top: 6mm; left: 0.5mm; width: 4mm; height: 0.2mm; }
     691#as-print-host .as-crop-tr-h { top: 6mm; right: 0.5mm; width: 4mm; height: 0.2mm; }
     692#as-print-host .as-crop-bl-h { bottom: 6mm; left: 0.5mm; width: 4mm; height: 0.2mm; }
     693#as-print-host .as-crop-br-h { bottom: 6mm; right: 0.5mm; width: 4mm; height: 0.2mm; }
     694
     695#as-print-host .as-crop-tl-v { top: 0.5mm; left: 6mm; width: 0.2mm; height: 4mm; }
     696#as-print-host .as-crop-tr-v { top: 0.5mm; right: 6mm; width: 0.2mm; height: 4mm; }
     697#as-print-host .as-crop-bl-v { bottom: 0.5mm; left: 6mm; width: 0.2mm; height: 4mm; }
     698#as-print-host .as-crop-br-v { bottom: 0.5mm; right: 6mm; width: 0.2mm; height: 4mm; }
     699
     700@media (max-width: 980px) {
    315701  #awosign-root main {
    316     grid-template-columns:1fr
    317   }
    318   #awosign-root .panel {
    319     min-width:0
    320   }
    321 }
    322 @media(max-width:560px) {
    323   #awosign-root main {
    324     padding:10px
    325   }
    326   #awosign-root .panel {
    327     padding:14px
    328   }
    329   #awosign-root .stage {
    330     padding:12px
    331   }
    332   #awosign-root .two {
    333     grid-template-columns:1fr
    334   }
    335   #awosign-root .sigrow {
    336     gap:8px
    337   }
    338   #awosign-root .siglogo {
    339     width:32%;
    340     min-width:0
    341   }
    342   #awosign-root .siginfo {
    343     padding-left:9px;
    344     font-size:10px
    345   }
    346   #awosign-root .siginfo strong {
    347     font-size:15px
    348   }
    349   #awosign-root .card {
    350     padding:15px
    351   }
    352   #awosign-root .back strong {
    353     font-size:16px
    354   }
    355   #awosign-root .back .details {
    356     font-size:10px
    357   }
    358   #awosign-root .qr {
    359     width:80px;
    360     height:80px
    361   }
    362   #awosign-root .digital {
    363     padding:16px;
    364     min-height:260px
    365   }
    366   #awosign-root .digital strong {
    367     font-size:18px
    368   }
    369 }
    370 
    371 
    372 #awosign-root {
    373   margin:20px 20px 20px 0;
    374   min-height:80vh
    375 }
    376 #awosign-root button:focus-visible,#awosign-root input:focus-visible {
    377   outline:3px solid #e7b52e;
    378   outline-offset:3px
    379 }
    380 #awosign-root .signature {
    381   box-sizing:border-box
    382 }
    383 #awosign-root p {
    384   font-size:13px
    385 }
    386 #awosign-root .privacy {
    387   padding:12px 22px;
    388   background:#102032;
    389   color:#d7e4f5
    390 }
    391 #awosign-root h1,#awosign-root h2,#awosign-root h3 {
    392   color:inherit
    393 }
    394 #awosign-root .siglogo div {
    395   overflow-wrap:anywhere
    396 }
    397 @media(max-width:560px) {
    398   #awosign-root .sigrow {
    399     display:block
    400   }
    401   #awosign-root .siglogo {
    402     width:100%;
    403     text-align:left;
    404     margin-bottom:12px
    405   }
    406   #awosign-root .siginfo {
    407     border-left:0;
    408     border-top:2px solid var(--accent);
    409     padding:10px 0 0;
    410     font-size:12px
    411   }
    412   #awosign-root .siginfo strong {
    413     font-size:18px
    414   }
    415 }
     702    grid-template-columns: 1fr;
     703  }
     704  #awosign-root .as-print-grid {
     705    grid-template-columns: 1fr;
     706  }
     707}
     708
     709@media print {
     710  html,
     711  body {
     712    margin: 0 !important;
     713    padding: 0 !important;
     714    background: #ffffff !important;
     715  }
     716
     717  body > *:not(#as-print-host),
     718  #wpadminbar,
     719  #adminmenumain,
     720  #adminmenuback,
     721  #adminmenuwrap,
     722  #wpfooter {
     723    display: none !important;
     724  }
     725
     726  body #as-print-host {
     727    display: block !important;
     728    margin: 0 !important;
     729    padding: 0 !important;
     730  }
     731
     732  #as-print-host > .card {
     733    width: 85mm;
     734    height: 55mm;
     735    margin: 0;
     736    box-shadow: none !important;
     737    break-after: page;
     738    page-break-after: always;
     739    -webkit-print-color-adjust: exact;
     740    print-color-adjust: exact;
     741  }
     742
     743  #as-print-host > .card:last-child {
     744    break-after: auto;
     745    page-break-after: auto;
     746  }
     747
     748  #as-print-host .card {
     749    box-shadow: none !important;
     750    -webkit-print-color-adjust: exact;
     751    print-color-adjust: exact;
     752  }
     753}
  • awosign/trunk/assets/designer.js

    r3783841 r3783861  
    1 /* AwoSign Free 1.0.1 — GPL-2.0-or-later */
     1/* AwoSign Free 1.3.0 — GPL-2.0-or-later */
    22(function () {
    33  "use strict";
    … …  
    55  const root = document.getElementById('awosign-root');
    66  if (!root) return;
    7   const $ = id => root.querySelector('[id="' + id + '"]'),
    8     fields = ['company', 'person', 'job', 'phone', 'email', 'website', 'address', 'qrUrl'];
     7
     8  const $ = id => root.querySelector('[id="' + id + '"]');
     9  const brandFields = ['company', 'brand', 'accent', 'website', 'address', 'logoUrl', 'sigTemplate', 'language', 'theme'];
     10  const staffFields = ['person', 'job', 'phone', 'email', 'qrMode', 'qrUrl'];
     11  const STORAGE_KEY = 'awosign_workspace_v130';
     12
    913  let logo = '';
    10   const platforms = [['LinkedIn', 'in', 'https://www.linkedin.com/in/'], ['Instagram', '◎', 'https://www.instagram.com/'], ['Facebook', 'f', 'https://www.facebook.com/'], ['X', 'X', 'https://x.com/'], ['YouTube', '▶', 'https://www.youtube.com/@'], ['WhatsApp', '☎', 'https://wa.me/'], ['TikTok', '♪', 'https://www.tiktok.com/@'], ['Pinterest', 'P', 'https://www.pinterest.com/'], ['GitHub', 'GH', 'https://github.com/'], ['Telegram', '✈', 'https://t.me/']];
    1114  let active = 'Signature';
    12   // Escape contact text before inserting it into preview or exported HTML.
     15
     16  const platforms = [
     17    ['LinkedIn', 'in', 'https://www.linkedin.com/in/'],
     18    ['Instagram', '◎', 'https://www.instagram.com/'],
     19    ['Facebook', 'f', 'https://www.facebook.com/'],
     20    ['X', 'X', 'https://x.com/'],
     21    ['YouTube', '▶', 'https://www.youtube.com/@'],
     22    ['WhatsApp', '☎', 'https://wa.me/'],
     23    ['TikTok', '♪', 'https://www.tiktok.com/@'],
     24    ['Pinterest', 'P', 'https://www.pinterest.com/'],
     25    ['GitHub', 'GH', 'https://github.com/'],
     26    ['Telegram', '✈', 'https://t.me/']
     27  ];
     28
     29  let workspace = {
     30    version: 3,
     31    activeProfileId: 'prof_1',
     32    brand: {
     33      company: 'Example Studio',
     34      brand: '#172e49',
     35      accent: '#e7b52e',
     36      website: 'https://example.com',
     37      address: '123 Example Street, Istanbul, TR',
     38      logo: '',
     39      logoUrl: '',
     40      sigTemplate: 'classic',
     41      language: 'tr',
     42      theme: 'auto'
     43    },
     44    profiles: [
     45      {
     46        id: 'prof_1',
     47        person: 'Alex Morgan',
     48        job: 'Creative Director',
     49        phone: '+90 555 123 4567',
     50        email: 'alex@example.com',
     51        qrMode: 'url',
     52        qrUrl: '',
     53        social: {
     54          LinkedIn: { enabled: true, value: 'example' },
     55          Instagram: { enabled: true, value: 'example' },
     56          Facebook: { enabled: true, value: 'example' }
     57        }
     58      }
     59    ]
     60  };
     61
    1362  function esc(v) {
    1463    return String(v || '').replace(/[&<>"']/g, c => ({
    … …  
    2069    })[c]);
    2170  }
     71
    2272  function val(k) {
    23     return $(k).value.trim();
    24   }
    25   // Only HTTP(S) links are accepted for profiles and QR destinations.
     73    const el = $(k);
     74    return el ? el.value.trim() : '';
     75  }
     76
     77  function isShowCrop() {
     78    const el = $('cropMarks');
     79    if (!el) return true;
     80    if (el.type === 'checkbox') return el.checked;
     81    return el.value !== '0';
     82  }
     83
     84  function setStatus(msg) {
     85    const s = $('as-status');
     86    if (s) s.textContent = msg || '';
     87  }
     88
    2689  function validUrl(s) {
    2790    try {
    28       let u = new URL(s);
     91      const u = new URL(s);
    2992      return ['https:', 'http:'].includes(u.protocol) ? u.href : '';
    3093    } catch {
    … …  
    3295    }
    3396  }
     97
    3498  function profile(p) {
    35     let s = $('soc_' + p[0]).value.trim();
     99    const inputEl = $('soc_' + p[0]);
     100    let s = inputEl ? inputEl.value.trim() : '';
    36101    if (!s) return '';
    37102    if (/^https?:\/\//i.test(s)) return validUrl(s);
    … …  
    39104    return validUrl(p[2] + encodeURIComponent(s.replace(/^@/, '')));
    40105  }
     106
    41107  function socialHtml() {
    42     return platforms.filter(p => $('check_' + p[0]).checked && profile(p)).map(p => '<a href="' + esc(profile(p)) + '" target="_blank" rel="noopener noreferrer" style="text-decoration:none"><span>' + esc(p[1]) + '</span></a>').join('');
    43   }
    44   function logoHtml(style) {
    45     return logo ? '<img src="' + logo + '" alt="Company logo" style="' + style + '">' : '<div style="font-size:24px;font-weight:bold">' + esc(val('company')) + '</div>';
    46   }
    47   let qrKey = '',
    48     qrMarkup = '';
    49   // Generate and cache QR images locally using the bundled MIT library.
     108    return platforms
     109      .filter(p => $('check_' + p[0]) && $('check_' + p[0]).checked && profile(p))
     110      .map(p => '<a href="' + esc(profile(p)) + '" target="_blank" rel="noopener noreferrer" style="text-decoration:none"><span>' + esc(p[1]) + '</span></a>')
     111      .join('');
     112  }
     113
     114  function getSignatureLogoSrc() {
     115    return validUrl(val('logoUrl')) || logo || '';
     116  }
     117
     118  function logoHtml(style, preferUrlForEmail) {
     119    const src = preferUrlForEmail ? getSignatureLogoSrc() : (logo || validUrl(val('logoUrl')));
     120    return src
     121      ? '<img src="' + esc(src) + '" alt="Company logo" style="' + style + '">'
     122      : '<div style="font-size:22px;font-weight:bold">' + esc(val('company')) + '</div>';
     123  }
     124
     125  function getActiveProfile() {
     126    let found = workspace.profiles.find(p => p.id === workspace.activeProfileId);
     127    if (!found) {
     128      found = workspace.profiles[0];
     129      workspace.activeProfileId = found.id;
     130    }
     131    return found;
     132  }
     133
     134  function syncFormToWorkspace() {
     135    brandFields.forEach(k => {
     136      if ($(k)) workspace.brand[k] = val(k);
     137    });
     138    workspace.brand.logo = logo;
     139
     140    const current = getActiveProfile();
     141    if (current) {
     142      staffFields.forEach(k => {
     143        if ($(k)) current[k] = val(k);
     144      });
     145      current.social = {};
     146      platforms.forEach(p => {
     147        const chk = $('check_' + p[0]);
     148        const inp = $('soc_' + p[0]);
     149        current.social[p[0]] = {
     150          enabled: chk ? chk.checked : false,
     151          value: inp ? inp.value.trim() : ''
     152        };
     153      });
     154    }
     155    refreshProfileSelectLabels();
     156    try {
     157      localStorage.setItem(STORAGE_KEY, JSON.stringify(workspace));
     158    } catch (_e) {}
     159  }
     160
     161  function loadWorkspaceIntoForm() {
     162    if (workspace.brand) {
     163      brandFields.forEach(k => {
     164        if ($(k) && typeof workspace.brand[k] === 'string') {
     165          $(k).value = workspace.brand[k];
     166        }
     167      });
     168      logo = workspace.brand.logo || '';
     169    }
     170    const current = getActiveProfile();
     171    if (current) {
     172      staffFields.forEach(k => {
     173        if ($(k)) $(k).value = current[k] || (k === 'qrMode' ? 'url' : '');
     174      });
     175      platforms.forEach(p => {
     176        const s = current.social && current.social[p[0]];
     177        if ($('check_' + p[0])) $('check_' + p[0]).checked = !!(s && s.enabled);
     178        if ($('soc_' + p[0])) $('soc_' + p[0]).value = s && typeof s.value === 'string' ? s.value : '';
     179      });
     180    }
     181    rebuildProfileSelect();
     182    applyLanguageText();
     183    applyTheme();
     184  }
     185
     186  function rebuildProfileSelect() {
     187    const sel = $('profileSelect');
     188    if (!sel) return;
     189    sel.innerHTML = '';
     190    workspace.profiles.forEach((p, idx) => {
     191      const opt = document.createElement('option');
     192      opt.value = p.id;
     193      opt.textContent = (p.person || ('Personel #' + (idx + 1))) + (p.job ? ' — ' + p.job : '');
     194      if (p.id === workspace.activeProfileId) opt.selected = true;
     195      sel.appendChild(opt);
     196    });
     197    const badge = $('profileCountBadge');
     198    if (badge) {
     199      const count = workspace.profiles.length;
     200      badge.textContent = val('language') === 'en' ? count + (count === 1 ? ' Profile' : ' Profiles') : count + ' Profil';
     201    }
     202    const sc = $('shortcodeDisplay');
     203    if (sc) {
     204      sc.value = '[awosign id="' + workspace.activeProfileId + '"]';
     205    }
     206    if ($('deleteProfile')) {
     207      $('deleteProfile').disabled = workspace.profiles.length <= 1;
     208    }
     209  }
     210
     211  function refreshProfileSelectLabels() {
     212    const sel = $('profileSelect');
     213    if (!sel) return;
     214    Array.from(sel.options).forEach((opt, idx) => {
     215      const p = workspace.profiles[idx];
     216      if (p) {
     217        opt.textContent = (p.person || ('Personel #' + (idx + 1))) + (p.job ? ' — ' + p.job : '');
     218      }
     219    });
     220    const sc = $('shortcodeDisplay');
     221    if (sc) {
     222      sc.value = '[awosign id="' + workspace.activeProfileId + '"]';
     223    }
     224  }
     225
     226  function buildVCardPayload(compactForQr) {
     227    const clean = v =>
     228      String(v || '')
     229        .replace(/\\/g, '\\\\')
     230        .replace(/\r\n|\r|\n/g, '\\n')
     231        .replace(/[,;]/g, c => '\\' + c);
     232
     233    const lines = [
     234      'BEGIN:VCARD',
     235      'VERSION:3.0',
     236      'FN:' + clean(val('person')),
     237      'ORG:' + clean(val('company')),
     238      'TITLE:' + clean(val('job')),
     239      val('phone') ? 'TEL;TYPE=WORK,VOICE:' + clean(val('phone')) : '',
     240      val('email') ? 'EMAIL:' + clean(val('email')) : '',
     241      validUrl(val('website')) ? 'URL:' + clean(validUrl(val('website'))) : '',
     242      !compactForQr && val('address') ? 'ADR;TYPE=WORK:;;' + clean(val('address')) + ';;;;' : '',
     243      'END:VCARD'
     244    ].filter(Boolean);
     245    return lines.join('\r\n');
     246  }
     247
     248  let qrKey = '';
     249  let qrMarkup = '';
     250  let lastQrDataUrl = '';
     251
    50252  function qrHtml() {
    51     const url = validUrl(val('qrUrl')) || validUrl(val('website'));
    52     if (url === qrKey && qrMarkup) return qrMarkup;
    53     qrKey = url;
     253    const qrMode = val('qrMode') || 'url';
     254    const payload =
     255      qrMode === 'vcard'
     256        ? buildVCardPayload(true)
     257        : (validUrl(val('qrUrl')) || validUrl(val('website')));
     258
     259    const cacheKey = qrMode + ':' + payload;
     260    if (cacheKey === qrKey && qrMarkup) return qrMarkup;
     261    qrKey = cacheKey;
     262
    54263    try {
    55       if (!url) throw new Error('URL required');
    56       const code = qrcode(0, 'M');
    57       code.addData(url, 'Byte');
     264      if (!payload) throw new Error('Payload required');
     265      const code = qrcode(0, 'L');
     266      code.addData(payload, 'Byte');
    58267      code.make();
    59       qrMarkup = '<img class="qr" src="' + code.createDataURL(5, 8) + '" alt="QR code">';
    60     } catch (e) {
    61       qrMarkup = '<span class="qr" style="color:#333;font-size:11px">' + (val('language') === 'en' ? 'QR unavailable: check URL' : 'QR oluşturulamadı: adresi kontrol edin') + '</span>';
     268      lastQrDataUrl = code.createDataURL(5, 6);
     269      qrMarkup = '<img class="qr" src="' + lastQrDataUrl + '" alt="QR code">';
     270    } catch (_e) {
     271      lastQrDataUrl = '';
     272      qrMarkup =
     273        '<span class="qr" style="color:#333;font-size:10px;display:flex;align-items:center;justify-content:center;text-align:center">' +
     274        (val('language') === 'en' ? 'QR unavailable: check URL' : 'QR oluşturulamadı: adresi kontrol edin') +
     275        '</span>';
    62276    }
    63277    return qrMarkup;
    64278  }
    65   // Update the signature, both print faces, and digital contact preview.
     279
     280  function applyTheme() {
     281    const tMode = val('theme') || 'auto';
     282    root.setAttribute('data-theme', tMode);
     283  }
     284
     285  function applyLanguageText() {
     286    const l = val('language') || 'tr';
     287    root.lang = l;
     288    root.querySelectorAll('[data-tr]').forEach(x => {
     289      x.textContent = x.dataset[l];
     290    });
     291  }
     292
     293  /**
     294   * Build Table-based Email Signature HTML for Selected Template (classic | compact | banner)
     295   */
     296  function signatureHtml() {
     297    const brand = val('brand') || '#172e49';
     298    const accent = val('accent') || '#e7b52e';
     299    const tpl = val('sigTemplate') || 'classic';
     300    const name = esc(val('person'));
     301    const job = esc(val('job'));
     302    const company = esc(val('company'));
     303    const phone = esc(val('phone'));
     304    const email = esc(val('email'));
     305    const web = esc(val('website'));
     306    const addr = esc(val('address'));
     307    const badges = socialHtml().replace(
     308      /<span>/g,
     309      '<span style="display:inline-block;margin:2px;padding:4px 6px;border-radius:3px;background:' +
     310        brand +
     311        ';color:white;font-size:11px;font-weight:bold">'
     312    );
     313
     314    if (tpl === 'compact') {
     315      return (
     316        '<table role="presentation" cellpadding="0" cellspacing="0" border="0" style="width:520px;max-width:100%;font-family:Arial,Helvetica,sans-serif;color:#15263c;background:#fff;border-top:4px solid ' +
     317        brand +
     318        ';border-bottom:2px solid ' +
     319        accent +
     320        '"><tr><td style="padding:14px 16px;vertical-align:middle;width:130px">' +
     321        logoHtml('max-width:115px;max-height:70px;width:auto;height:auto', true) +
     322        '</td><td style="padding:14px 16px;font-size:12px;line-height:1.5"><strong style="font-size:16px;color:' +
     323        brand +
     324        '">' +
     325        name +
     326        '</strong> · <span style="color:#475569;font-weight:bold">' +
     327        job +
     328        '</span><br><span style="color:' +
     329        brand +
     330        ';font-weight:bold">' +
     331        company +
     332        '</span><br>' +
     333        phone +
     334        ' · ' +
     335        email +
     336        '<br>' +
     337        web +
     338        '<div style="margin-top:6px">' +
     339        badges +
     340        '</div></td></tr></table>'
     341      );
     342    }
     343
     344    if (tpl === 'banner') {
     345      const qrImg = lastQrDataUrl
     346        ? '<img src="' + lastQrDataUrl + '" alt="QR" style="width:78px;height:78px;display:block;border:1px solid #e2e8f0;padding:3px;border-radius:4px">'
     347        : '';
     348      return (
     349        '<table role="presentation" cellpadding="0" cellspacing="0" border="0" style="width:600px;max-width:100%;font-family:Arial,Helvetica,sans-serif;color:#15263c;background:#fff;border:1px solid #dbe3ec;border-radius:6px;overflow:hidden"><tr style="background:' +
     350        brand +
     351        ';color:#fff"><td colspan="2" style="padding:12px 18px;border-bottom:3px solid ' +
     352        accent +
     353        '"><table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td><strong style="font-size:18px;color:#fff">' +
     354        name +
     355        '</strong><br><span style="font-size:12px;color:#dbe7f5">' +
     356        job +
     357        ' · ' +
     358        company +
     359        '</span></td><td align="right">' +
     360        logoHtml('max-width:110px;max-height:46px;width:auto;height:auto', true) +
     361        '</td></tr></table></td></tr><tr><td style="padding:14px 18px;font-size:12px;line-height:1.6;vertical-align:middle">' +
     362        '📞 ' +
     363        phone +
     364        '<br>✉️ ' +
     365        email +
     366        '<br>🌐 ' +
     367        web +
     368        '<br>📍 ' +
     369        addr +
     370        '<div style="margin-top:8px">' +
     371        badges +
     372        '</div></td><td style="padding:14px 18px;width:96px;vertical-align:middle" align="right">' +
     373        qrImg +
     374        '</td></tr></table>'
     375      );
     376    }
     377
     378    return (
     379      '<table role="presentation" cellpadding="0" cellspacing="0" border="0" style="width:600px;max-width:100%;font-family:Arial,Helvetica,sans-serif;color:#15263c;background:#fff"><tr><td style="width:170px;padding:18px;vertical-align:middle">' +
     380      logoHtml('max-width:150px;max-height:100px;width:auto;height:auto', true) +
     381      '</td><td style="padding:18px;border-left:2px solid ' +
     382      accent +
     383      ';font-size:12px;line-height:1.6"><strong style="font-size:19px">' +
     384      name +
     385      '</strong><br>' +
     386      job +
     387      '<br>' +
     388      company +
     389      '<br>' +
     390      phone +
     391      '<br>' +
     392      email +
     393      '<br>' +
     394      web +
     395      '<div style="margin-top:8px">' +
     396      badges +
     397      '</div></td></tr><tr><td colspan="2" style="padding:12px 18px;border-top:1px solid #ddd;font-size:11px">' +
     398      addr +
     399      '</td></tr></table>'
     400    );
     401  }
     402
    66403  function render() {
    67     root.style.setProperty('--brand', $('brand').value);
    68     root.style.setProperty('--accent', $('accent').value);
    69     let name = esc(val('person')),
    70       company = esc(val('company')),
    71       job = esc(val('job')),
    72       phone = esc(val('phone')),
    73       email = esc(val('email')),
    74       web = esc(val('website')),
    75       address = esc(val('address'));
    76     let icons = socialHtml();
    77     $('sig').innerHTML = '<div class="sigrow"><div class="siglogo">' + logoHtml('max-width:100%;max-height:100px;object-fit:contain') + '</div><div class="siginfo"><strong>' + name + '</strong><br>' + job + '<br>' + company + '<br>' + phone + '<br>' + email + '<br>' + web + '<div class="socialicons">' + icons + '</div></div></div><div class="sigaddress">' + address + '</div>';
    78     $('front').innerHTML = logoHtml('max-width:55%;max-height:95px;object-fit:contain') + '<strong>' + company + '</strong><small>' + web + '</small>';
    79     $('back').innerHTML = '<div class="details"><strong>' + name + '</strong><br>' + job + '<br>' + company + '<br>' + phone + '<br>' + email + '<br>' + web + '<br>' + address + '<div class="socialicons">' + icons + '</div></div>' + qrHtml();
    80     $('digital').innerHTML = '<div class="details">' + logoHtml('max-width:100px;max-height:70px;object-fit:contain') + '<br><strong>' + name + '</strong><br>' + job + '<br>' + company + '<br>' + phone + '<br>' + email + '<br>' + web + '<div class="socialicons">' + icons + '</div></div>' + qrHtml();
    81   }
     404    syncFormToWorkspace();
     405    applyTheme();
     406    root.style.setProperty('--brand', val('brand') || '#172e49');
     407    root.style.setProperty('--accent', val('accent') || '#e7b52e');
     408
     409    const name = esc(val('person'));
     410    const company = esc(val('company'));
     411    const job = esc(val('job'));
     412    const phone = esc(val('phone'));
     413    const email = esc(val('email'));
     414    const web = esc(val('website'));
     415    const address = esc(val('address'));
     416    const icons = socialHtml();
     417    const showCrop = isShowCrop();
     418
     419    // Ensure QR cache is up-to-date before signatureHtml() uses lastQrDataUrl
     420    const qrMarkupNow = qrHtml();
     421
     422    $('sig').innerHTML = signatureHtml();
     423
     424    $('front').innerHTML =
     425      logoHtml('max-width:55%;max-height:85px;object-fit:contain', false) +
     426      '<strong>' +
     427      company +
     428      '</strong><small>' +
     429      web +
     430      '</small>';
     431
     432    $('back').innerHTML =
     433      '<div class="details"><strong>' +
     434      name +
     435      '</strong><br>' +
     436      job +
     437      '<br>' +
     438      company +
     439      '<br>' +
     440      phone +
     441      '<br>' +
     442      email +
     443      '<br>' +
     444      web +
     445      '<br>' +
     446      address +
     447      '<div class="socialicons">' +
     448      icons +
     449      '</div></div>' +
     450      qrMarkupNow;
     451
     452    $('front').classList.toggle('show-crop-guide', showCrop);
     453    $('back').classList.toggle('show-crop-guide', showCrop);
     454
     455    $('digital').innerHTML =
     456      '<div class="details">' +
     457      logoHtml('max-width:100px;max-height:70px;object-fit:contain', false) +
     458      '<br><strong>' +
     459      name +
     460      '</strong><br>' +
     461      job +
     462      '<br>' +
     463      company +
     464      '<br>' +
     465      phone +
     466      '<br>' +
     467      email +
     468      '<br>' +
     469      web +
     470      '<div class="socialicons">' +
     471      icons +
     472      '</div></div>' +
     473      qrMarkupNow;
     474  }
     475
    82476  function download(name, content, type) {
    83     let b = new Blob([content], {
    84       type
    85     });
    86     let a = document.createElement('a');
     477    const b = content instanceof Blob ? content : new Blob([content], { type });
     478    const a = document.createElement('a');
    87479    a.href = URL.createObjectURL(b);
    88480    a.download = name;
     481    document.body.appendChild(a);
    89482    a.click();
     483    a.remove();
    90484    setTimeout(() => URL.revokeObjectURL(a.href), 1000);
    91485  }
    92   // Export vCard 3.0 with escaped field separators and line breaks.
     486
     487  function loadImage(src) {
     488    return new Promise((resolve, reject) => {
     489      const i = new Image();
     490      i.crossOrigin = 'anonymous';
     491      i.onload = () => resolve(i);
     492      i.onerror = reject;
     493      i.src = src;
     494    });
     495  }
     496
    93497  function vcf() {
    94     const clean = v => String(v).replace(/\\/g, '\\\\').replace(/\r\n|\r|\n/g, '\\n').replace(/[,;]/g, c => '\\' + c);
    95     download('awosign-contact.vcf', ['BEGIN:VCARD', 'VERSION:3.0', 'FN:' + clean(val('person')), 'ORG:' + clean(val('company')), 'TITLE:' + clean(val('job')), 'TEL;TYPE=WORK,VOICE:' + clean(val('phone')), 'EMAIL:' + clean(val('email')), 'URL:' + clean(validUrl(val('website'))), 'ADR;TYPE=WORK:;;' + clean(val('address')) + ';;;;', 'END:VCARD'].join('\r\n'), 'text/vcard;charset=utf-8');
    96   }
     498    download('awosign-contact.vcf', buildVCardPayload(false), 'text/vcard;charset=utf-8');
     499    setStatus(val('language') === 'en' ? 'VCF contact card downloaded.' : 'VCF rehber kartı indirildi.');
     500  }
     501
     502  /**
     503   * High-DPI Canvas Renderer for Front & Back Print Cards (300 PPI or 600 DPI with 3mm Bleed & Crop Marks)
     504   */
     505  async function renderPrintFaceToPngBlob(face, withBleedAndCrop) {
     506    const scale = withBleedAndCrop ? 2 : 1;
     507    const trimW = 1004 * scale;
     508    const trimH = 650 * scale;
     509    const marginPx = withBleedAndCrop ? Math.round(71 * scale) : 0;
     510    const bleedPx = withBleedAndCrop ? Math.round(35.4 * scale) : 0;
     511
     512    const canvas = document.createElement('canvas');
     513    canvas.width = trimW + marginPx * 2;
     514    canvas.height = trimH + marginPx * 2;
     515    const ctx = canvas.getContext('2d');
     516
     517    const brand = val('brand') || '#172e49';
     518    const accent = val('accent') || '#e7b52e';
     519    const originX = marginPx;
     520    const originY = marginPx;
     521
     522    ctx.fillStyle = '#ffffff';
     523    ctx.fillRect(0, 0, canvas.width, canvas.height);
     524
     525    if (face === 'front') {
     526      ctx.fillStyle = brand;
     527      ctx.fillRect(
     528        originX - bleedPx,
     529        originY - bleedPx,
     530        trimW + bleedPx * 2,
     531        trimH + bleedPx * 2
     532      );
     533      ctx.fillStyle = accent;
     534      ctx.fillRect(
     535        originX - bleedPx,
     536        originY - bleedPx,
     537        trimW + bleedPx * 2,
     538        Math.round(18 * scale) + bleedPx
     539      );
     540
     541      let centerY = originY + Math.round(trimH * 0.42);
     542      const activeLogo = logo || validUrl(val('logoUrl'));
     543      if (activeLogo) {
     544        try {
     545          const img = await loadImage(activeLogo);
     546          const maxW = 520 * scale;
     547          const maxH = 220 * scale;
     548          const ratio = Math.min(maxW / img.width, maxH / img.height, 1);
     549          const drawW = img.width * ratio;
     550          const drawH = img.height * ratio;
     551          ctx.drawImage(
     552            img,
     553            originX + (trimW - drawW) / 2,
     554            originY + Math.round(135 * scale),
     555            drawW,
     556            drawH
     557          );
     558          centerY = originY + Math.round(155 * scale) + drawH + Math.round(55 * scale);
     559        } catch (_e) {}
     560      }
     561
     562      ctx.fillStyle = '#ffffff';
     563      ctx.textAlign = 'center';
     564      ctx.font = `bold ${Math.round(52 * scale)}px Arial, sans-serif`;
     565      ctx.fillText(val('company'), originX + trimW / 2, centerY);
     566
     567      ctx.fillStyle = '#dbe7f5';
     568      ctx.font = `${Math.round(28 * scale)}px Arial, sans-serif`;
     569      ctx.fillText(val('website'), originX + trimW / 2, centerY + Math.round(48 * scale));
     570    } else {
     571      ctx.fillStyle = '#ffffff';
     572      ctx.fillRect(
     573        originX - bleedPx,
     574        originY - bleedPx,
     575        trimW + bleedPx * 2,
     576        trimH + bleedPx * 2
     577      );
     578      ctx.fillStyle = accent;
     579      ctx.fillRect(
     580        originX - bleedPx,
     581        originY - bleedPx,
     582        Math.round(22 * scale) + bleedPx,
     583        trimH + bleedPx * 2
     584      );
     585
     586      const padX = originX + Math.round(68 * scale);
     587      let curY = originY + Math.round(115 * scale);
     588
     589      ctx.textAlign = 'left';
     590      ctx.fillStyle = '#102032';
     591      ctx.font = `bold ${Math.round(46 * scale)}px Arial, sans-serif`;
     592      ctx.fillText(val('person'), padX, curY);
     593
     594      curY += Math.round(42 * scale);
     595      ctx.fillStyle = '#243f5d';
     596      ctx.font = `bold ${Math.round(29 * scale)}px Arial, sans-serif`;
     597      ctx.fillText(val('job') + (val('company') ? ' · ' + val('company') : ''), padX, curY);
     598
     599      curY += Math.round(46 * scale);
     600      ctx.fillStyle = '#15263c';
     601      ctx.font = `${Math.round(27 * scale)}px Arial, sans-serif`;
     602      [val('phone'), val('email'), val('website'), val('address')].filter(Boolean).forEach(line => {
     603        ctx.fillText(line.slice(0, 52), padX, curY);
     604        curY += Math.round(38 * scale);
     605      });
     606
     607      const activeSocials = platforms.filter(p => $('check_' + p[0]) && $('check_' + p[0]).checked && profile(p));
     608      if (activeSocials.length) {
     609        let socX = padX;
     610        const socY = originY + trimH - Math.round(75 * scale);
     611        ctx.font = `bold ${Math.round(22 * scale)}px Arial, sans-serif`;
     612        activeSocials.forEach(p => {
     613          const badge = p[1];
     614          const w = Math.max(Math.round(42 * scale), ctx.measureText(badge).width + Math.round(24 * scale));
     615          ctx.fillStyle = brand;
     616          ctx.fillRect(socX, socY, w, Math.round(38 * scale));
     617          ctx.fillStyle = '#ffffff';
     618          ctx.fillText(badge, socX + Math.round(11 * scale), socY + Math.round(26 * scale));
     619          socX += w + Math.round(10 * scale);
     620        });
     621      }
     622
     623      if (lastQrDataUrl) {
     624        try {
     625          const qImg = await loadImage(lastQrDataUrl);
     626          const qSize = Math.round(255 * scale);
     627          ctx.drawImage(
     628            qImg,
     629            originX + trimW - Math.round(55 * scale) - qSize,
     630            originY + (trimH - qSize) / 2,
     631            qSize,
     632            qSize
     633          );
     634        } catch (_e) {}
     635      }
     636    }
     637
     638    if (withBleedAndCrop) {
     639      ctx.strokeStyle = '#000000';
     640      ctx.lineWidth = Math.max(1, Math.round(1.5 * scale));
     641      const markLen = Math.round(42 * scale);
     642      const markGap = Math.round(18 * scale);
     643      const drawCorner = (cx, cy, dx, dy) => {
     644        ctx.beginPath();
     645        ctx.moveTo(cx + dx * markGap, cy);
     646        ctx.lineTo(cx + dx * (markGap + markLen), cy);
     647        ctx.moveTo(cx, cy + dy * markGap);
     648        ctx.lineTo(cx, cy + dy * (markGap + markLen));
     649        ctx.stroke();
     650      };
     651      drawCorner(originX, originY, -1, -1);
     652      drawCorner(originX + trimW, originY, 1, -1);
     653      drawCorner(originX, originY + trimH, -1, 1);
     654      drawCorner(originX + trimW, originY + trimH, 1, 1);
     655    }
     656
     657    return new Promise(resolve => canvas.toBlob(resolve, 'image/png'));
     658  }
     659
     660  function applyDynamicPageSize(sizeCss) {
     661    let styleEl = document.getElementById('as-dynamic-page-style');
     662    if (!styleEl) {
     663      styleEl = document.createElement('style');
     664      styleEl.id = 'as-dynamic-page-style';
     665      document.head.appendChild(styleEl);
     666    }
     667    styleEl.textContent = `@page { size: ${sizeCss}; margin: 0; }`;
     668  }
     669
     670  function buildBleedSheet(cardEl, isFront) {
     671    const sheet = document.createElement('div');
     672    sheet.className = 'as-bleed-sheet';
     673
     674    const bg = document.createElement('div');
     675    bg.className = isFront ? 'as-bleed-bg-front' : 'as-bleed-bg-back';
     676    sheet.appendChild(bg);
     677
     678    const clone = cardEl.cloneNode(true);
     679    clone.classList.remove('show-crop-guide');
     680    sheet.appendChild(clone);
     681
     682    [
     683      'as-crop-tl-h', 'as-crop-tl-v',
     684      'as-crop-tr-h', 'as-crop-tr-v',
     685      'as-crop-bl-h', 'as-crop-bl-v',
     686      'as-crop-br-h', 'as-crop-br-v'
     687    ].forEach(cls => {
     688      const m = document.createElement('div');
     689      m.className = 'as-crop-mark ' + cls;
     690      sheet.appendChild(m);
     691    });
     692    return sheet;
     693  }
     694
     695  function printBusinessCards() {
     696    let host = document.getElementById('as-print-host');
     697    if (!host) {
     698      host = document.createElement('div');
     699      host.id = 'as-print-host';
     700      document.body.appendChild(host);
     701    }
     702    host.style.setProperty('--brand', val('brand') || '#172e49');
     703    host.style.setProperty('--accent', val('accent') || '#e7b52e');
     704
     705    const layout = val('printLayout') || 'a4_10up_duplex';
     706    const copies = Math.max(1, Math.min(20, Number(val('printCopies')) || 1));
     707    const showCrop = isShowCrop();
     708    const frontEl = $('front');
     709    const backEl = $('back');
     710    const nodes = [];
     711
     712    const makeA4SheetOf = cardEl => {
     713      const sheet = document.createElement('div');
     714      sheet.className = 'as-a4-sheet';
     715      for (let i = 0; i < 10; i++) {
     716        const cell = document.createElement('div');
     717        cell.className = 'as-a4-cell' + (showCrop ? ' has-crop-lines' : '');
     718        const clone = cardEl.cloneNode(true);
     719        clone.classList.remove('show-crop-guide');
     720        cell.appendChild(clone);
     721        sheet.appendChild(cell);
     722      }
     723      return sheet;
     724    };
     725
     726    if (layout === 'a4_10up_duplex') {
     727      applyDynamicPageSize('210mm 297mm');
     728      for (let c = 0; c < copies; c++) {
     729        nodes.push(makeA4SheetOf(frontEl));
     730        nodes.push(makeA4SheetOf(backEl));
     731      }
     732    } else if (layout === 'a4_10up_front') {
     733      applyDynamicPageSize('210mm 297mm');
     734      for (let c = 0; c < copies; c++) {
     735        nodes.push(makeA4SheetOf(frontEl));
     736      }
     737    } else if (layout === 'a4_10up_back') {
     738      applyDynamicPageSize('210mm 297mm');
     739      for (let c = 0; c < copies; c++) {
     740        nodes.push(makeA4SheetOf(backEl));
     741      }
     742    } else if (layout === 'single_bleed' || layout === 'bleed_91x61') {
     743      applyDynamicPageSize('97mm 67mm');
     744      for (let c = 0; c < copies; c++) {
     745        nodes.push(buildBleedSheet(frontEl, true));
     746        nodes.push(buildBleedSheet(backEl, false));
     747      }
     748    } else {
     749      applyDynamicPageSize('85mm 55mm');
     750      for (let c = 0; c < copies; c++) {
     751        const fCopy = frontEl.cloneNode(true);
     752        const bCopy = backEl.cloneNode(true);
     753        fCopy.classList.remove('show-crop-guide');
     754        bCopy.classList.remove('show-crop-guide');
     755        nodes.push(fCopy, bCopy);
     756      }
     757    }
     758
     759    host.replaceChildren(...nodes);
     760    window.print();
     761  }
     762
    97763  platforms.forEach((p, i) => {
    98     $('socials').insertAdjacentHTML('beforeend', '<label><input type="checkbox" id="check_' + p[0] + '" ' + (i < 3 ? 'checked' : '') + '>' + p[0] + '</label>');
    99     $('socialInputs').insertAdjacentHTML('beforeend', '<label>' + p[0] + ' — ' + p[2] + '</label><input id="soc_' + p[0] + '" placeholder="username / URL" value="' + (i < 3 ? 'example' : '') + '">');
     764    $('socials').insertAdjacentHTML(
     765      'beforeend',
     766      '<label><input type="checkbox" id="check_' + p[0] + '" ' + (i < 3 ? 'checked' : '') + '>' + p[0] + '</label>'
     767    );
     768    $('socialInputs').insertAdjacentHTML(
     769      'beforeend',
     770      '<label>' + p[0] + ' — ' + p[2] + '</label><input id="soc_' + p[0] + '" placeholder="username / URL" value="' + (i < 3 ? 'example' : '') + '">'
     771    );
    100772  });
    101   root.querySelectorAll('input,textarea,select').forEach(el => el.addEventListener('input', render));
     773
     774  if (typeof window.awosignWpConfig === 'object' && window.awosignWpConfig && window.awosignWpConfig.workspace) {
     775    workspace = window.awosignWpConfig.workspace;
     776  } else {
     777    try {
     778      const savedLocal = localStorage.getItem(STORAGE_KEY) || localStorage.getItem('awosign_workspace_v120');
     779      if (savedLocal) {
     780        const parsed = JSON.parse(savedLocal);
     781        if (parsed && Array.isArray(parsed.profiles) && parsed.profiles.length) {
     782          workspace = parsed;
     783        }
     784      }
     785    } catch (_e) {}
     786  }
     787
     788  loadWorkspaceIntoForm();
     789
     790  root.querySelectorAll('input,textarea,select').forEach(el => {
     791    if (el.id === 'profileSelect' || el.id === 'loadProject' || el.id === 'logoFile') return;
     792    el.addEventListener('input', render);
     793  });
     794  root.querySelectorAll('select,input[type="checkbox"]').forEach(el => {
     795    if (el.id === 'profileSelect') return;
     796    el.addEventListener('change', render);
     797  });
     798
     799  if ($('profileSelect')) {
     800    $('profileSelect').onchange = e => {
     801      workspace.activeProfileId = e.target.value;
     802      loadWorkspaceIntoForm();
     803      render();
     804      setStatus(
     805        val('language') === 'en'
     806          ? 'Switched to profile: ' + val('person')
     807          : 'Aktif personel değiştirildi: ' + val('person')
     808      );
     809    };
     810  }
     811
     812  if ($('addProfile')) {
     813    $('addProfile').onclick = () => {
     814      syncFormToWorkspace();
     815      const newId = 'prof_' + Date.now().toString(36);
     816      const count = workspace.profiles.length + 1;
     817      workspace.profiles.push({
     818        id: newId,
     819        person: val('language') === 'en' ? 'New Staff #' + count : 'Yeni Personel #' + count,
     820        job: val('language') === 'en' ? 'Team Member' : 'Uzman / Yetkili',
     821        phone: val('phone') || '+90 555 000 0000',
     822        email:
     823          'personel' +
     824          count +
     825          '@' +
     826          ((val('website') || 'example.com').replace(/^https?:\/\/(www\.)?/i, '').split('/')[0] || 'example.com'),
     827        qrMode: 'url',
     828        qrUrl: '',
     829        social: {}
     830      });
     831      workspace.activeProfileId = newId;
     832      loadWorkspaceIntoForm();
     833      render();
     834      setStatus(val('language') === 'en' ? 'New staff profile added.' : 'Yeni personel profili oluşturuldu.');
     835    };
     836  }
     837
     838  if ($('deleteProfile')) {
     839    $('deleteProfile').onclick = () => {
     840      if (workspace.profiles.length <= 1) return;
     841      workspace.profiles = workspace.profiles.filter(p => p.id !== workspace.activeProfileId);
     842      workspace.activeProfileId = workspace.profiles[0].id;
     843      loadWorkspaceIntoForm();
     844      render();
     845      setStatus(val('language') === 'en' ? 'Staff profile deleted.' : 'Personel profili silindi.');
     846    };
     847  }
     848
     849  if ($('copyShortcode')) {
     850    $('copyShortcode').onclick = async () => {
     851      const code = $('shortcodeDisplay') ? $('shortcodeDisplay').value : '[awosign]';
     852      try {
     853        await navigator.clipboard.writeText(code);
     854        setStatus(val('language') === 'en' ? 'Shortcode copied: ' + code : 'Kısa kod kopyalandı: ' + code);
     855      } catch (_e) {
     856        if ($('shortcodeDisplay')) $('shortcodeDisplay').select();
     857        setStatus(code);
     858      }
     859    };
     860  }
     861
     862  if ($('pickWpMediaLogo')) {
     863    $('pickWpMediaLogo').onclick = () => {
     864      if (typeof window.wp !== 'undefined' && window.wp && window.wp.media) {
     865        const frame = window.wp.media({
     866          title: val('language') === 'en' ? 'Select Company Logo' : 'Şirket Logosu Seçin',
     867          multiple: false,
     868          library: { type: 'image' }
     869        });
     870        frame.on('select', () => {
     871          const attachment = frame.state().get('selection').first().toJSON();
     872          if (attachment && attachment.url) {
     873            if ($('logoUrl')) $('logoUrl').value = attachment.url;
     874            render();
     875            setStatus(val('language') === 'en' ? 'Media library logo selected.' : 'Medya kütüphanesi logosu seçildi.');
     876          }
     877        });
     878        frame.open();
     879      } else {
     880        setStatus(
     881          val('language') === 'en'
     882            ? 'WP Media Library is available inside WordPress admin. Paste any image URL in the field.'
     883            : 'WP Medya Kütüphanesi WordPress yönetim panelinde aktif olur. Kutuya doğrudan görsel URL yapıştırabilirsiniz.'
     884        );
     885      }
     886    };
     887  }
     888
     889  if ($('shareWhatsapp')) {
     890    $('shareWhatsapp').onclick = () => {
     891      const text = [
     892        '📇 *' + val('person') + '*',
     893        val('job') + (val('company') ? ' · ' + val('company') : ''),
     894        val('phone') ? '📞 ' + val('phone') : '',
     895        val('email') ? '✉️ ' + val('email') : '',
     896        val('website') ? '🌐 ' + val('website') : '',
     897        val('address') ? '📍 ' + val('address') : ''
     898      ]
     899        .filter(Boolean)
     900        .join('\n');
     901      window.open('https://wa.me/?text=' + encodeURIComponent(text), '_blank', 'noopener,noreferrer');
     902    };
     903  }
     904
     905  if ($('saveWpDb')) {
     906    $('saveWpDb').onclick = async () => {
     907      syncFormToWorkspace();
     908      if (typeof window.awosignWpConfig === 'object' && window.awosignWpConfig && window.awosignWpConfig.ajaxUrl) {
     909        try {
     910          setStatus(val('language') === 'en' ? 'Saving to WordPress...' : 'WordPress veritabanına kaydediliyor...');
     911          const body = new URLSearchParams();
     912          body.set('action', 'awosign_save_workspace');
     913          body.set('nonce', window.awosignWpConfig.nonce || '');
     914          body.set('workspace', JSON.stringify(workspace));
     915          const res = await fetch(window.awosignWpConfig.ajaxUrl, {
     916            method: 'POST',
     917            headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
     918            body: body.toString()
     919          });
     920          const json = await res.json();
     921          if (json && json.success) {
     922            setStatus(
     923              val('language') === 'en'
     924                ? 'Saved to WordPress database (' + workspace.profiles.length + ' profiles).'
     925                : 'WordPress veritabanına kaydedildi (' + workspace.profiles.length + ' personel profili).'
     926            );
     927          } else {
     928            throw new Error((json && json.data && json.data.message) || 'Save error');
     929          }
     930        } catch (err) {
     931          setStatus(
     932            val('language') === 'en'
     933              ? 'WordPress save failed: ' + err.message
     934              : 'WordPress kaydı başarısız: ' + err.message
     935          );
     936        }
     937      } else {
     938        setStatus(
     939          val('language') === 'en'
     940            ? 'Saved in browser storage (Preview mode). In WordPress this saves to wp_options.'
     941            : 'Tarayıcı belleğine kaydedildi (Önizleme modu). WordPress içinde wp_options veritabanına kaydedilir.'
     942        );
     943      }
     944    };
     945  }
     946
     947  if ($('useSiteLogo')) {
     948    $('useSiteLogo').onclick = async () => {
     949      const wpLogo =
     950        typeof window.awosignWpConfig === 'object' &&
     951        window.awosignWpConfig &&
     952        window.awosignWpConfig.siteLogoUrl
     953          ? window.awosignWpConfig.siteLogoUrl
     954          : '';
     955      if (wpLogo) {
     956        if ($('logoUrl') && !$('logoUrl').value) $('logoUrl').value = wpLogo;
     957        try {
     958          const img = await loadImage(wpLogo);
     959          const c = document.createElement('canvas');
     960          c.width = img.naturalWidth || img.width || 300;
     961          c.height = img.naturalHeight || img.height || 120;
     962          c.getContext('2d').drawImage(img, 0, 0);
     963          logo = c.toDataURL('image/png');
     964        } catch (_e) {
     965          logo = wpLogo;
     966        }
     967        render();
     968        setStatus(val('language') === 'en' ? 'WordPress site logo loaded.' : 'WordPress site logosu yüklendi.');
     969        return;
     970      }
     971      const comp = val('company') || 'AWO';
     972      const initials =
     973        comp
     974          .split(/\s+/)
     975          .filter(Boolean)
     976          .slice(0, 2)
     977          .map(w => w[0].toUpperCase())
     978          .join('') || 'AW';
     979      const c = document.createElement('canvas');
     980      c.width = 360;
     981      c.height = 140;
     982      const ctx = c.getContext('2d');
     983      ctx.fillStyle = val('accent') || '#e7b52e';
     984      ctx.beginPath();
     985      ctx.roundRect(10, 15, 110, 110, 22);
     986      ctx.fill();
     987      ctx.fillStyle = val('brand') || '#172e49';
     988      ctx.font = 'bold 50px Arial, sans-serif';
     989      ctx.textAlign = 'center';
     990      ctx.textBaseline = 'middle';
     991      ctx.fillText(initials, 65, 72);
     992      ctx.fillStyle = '#ffffff';
     993      ctx.font = 'bold 34px Arial, sans-serif';
     994      ctx.textAlign = 'left';
     995      ctx.fillText(comp.slice(0, 14), 138, 72);
     996      logo = c.toDataURL('image/png');
     997      render();
     998      setStatus(
     999        val('language') === 'en'
     1000          ? 'Generated company emblem logo.'
     1001          : 'Kurumsal amblem logosu otomatik oluşturuldu.'
     1002      );
     1003    };
     1004  }
     1005
    1021006  $('logoFile').addEventListener('change', e => {
    103     let f = e.target.files[0];
     1007    const f = e.target.files[0];
    1041008    if (!f) return;
    1051009    if (!['image/png', 'image/jpeg'].includes(f.type) || f.size > 3e6) {
    106       alert('PNG veya JPG · en fazla 3 MB');
     1010      setStatus(val('language') === 'en' ? 'Choose PNG/JPG up to 3 MB.' : 'En çok 3 MB PNG veya JPG seçin.');
    1071011      return;
    1081012    }
    109     let r = new FileReader();
     1013    const r = new FileReader();
    1101014    r.onload = () => {
    1111015      logo = r.result;
    1121016      render();
     1017      setStatus(val('language') === 'en' ? 'Logo updated.' : 'Logo güncellendi.');
    1131018    };
    1141019    r.readAsDataURL(f);
    1151020  });
    116   root.querySelectorAll('[data-tab]').forEach(b => b.onclick = () => {
    117     active = b.dataset.tab;
    118     root.querySelectorAll('[data-tab]').forEach(x => x.classList.toggle('active', x === b));
    119     ['Signature', 'Digital', 'Print'].forEach(t => $('view' + t).classList.toggle('hidden', t !== active));
     1021
     1022  root.querySelectorAll('[data-tab]').forEach(b => {
     1023    b.onclick = () => {
     1024      active = b.dataset.tab;
     1025      root.querySelectorAll('[data-tab]').forEach(x => x.classList.toggle('active', x === b));
     1026      ['Signature', 'Digital', 'Print'].forEach(t => $('view' + t).classList.toggle('hidden', t !== active));
     1027    };
    1201028  });
     1029
    1211030  $('language').onchange = () => {
    122     let l = val('language');
    123     root.lang = l;
    124     root.querySelectorAll('[data-tr]').forEach(x => x.textContent = x.dataset[l]);
     1031    applyLanguageText();
     1032    rebuildProfileSelect();
    1251033    render();
    1261034  };
    127   $('theme').onchange = () => root.classList.toggle('light', val('theme') === 'light');
     1035
     1036  $('theme').onchange = () => {
     1037    applyTheme();
     1038    syncFormToWorkspace();
     1039  };
    1281040  $('vcf').onclick = vcf;
    1291041  $('vcf2').onclick = vcf;
    130   $('print').onclick = () => {
    131     const w = window.open('', '_blank');
    132     if (!w) {
    133       alert('Please allow popups for printing.');
    134       return;
    135     }
    136     w.opener = null;
    137     w.document.write('<!doctype html><html><head><meta charset="utf-8"><title>AwoSign Print</title><style>@page{size:91mm 61mm;margin:0}*{box-sizing:border-box}body{margin:0;font-family:Arial,sans-serif}.card{width:91mm;height:61mm;padding:9mm;overflow:hidden;break-after:page;print-color-adjust:exact;-webkit-print-color-adjust:exact}.front{background:' + val('brand') + ';color:white;display:flex;flex-direction:column;align-items:center;justify-content:center;border-top:3mm solid ' + val('accent') + '}.back{background:white;color:#15263c;display:flex;align-items:center;justify-content:space-between;border-left:3mm solid ' + val('accent') + ';break-after:auto}.details{font-size:9pt;line-height:1.4;overflow-wrap:anywhere}.qr{width:22mm;height:22mm}.socialicons{display:flex;gap:3px}.socialicons span{font-size:8pt}small{font-size:8pt}strong{font-size:14pt}</style></head><body>' + document.getElementById('front').outerHTML + document.getElementById('back').outerHTML + '</body></html>');
    138     w.document.close();
    139     const imgs = Array.from(w.document.images);
    140     Promise.all(imgs.map(img => img.complete ? Promise.resolve() : new Promise(resolve => {
    141       img.onload = resolve;
    142       img.onerror = resolve;
    143     }))).then(() => {
    144       w.focus();
    145       w.print();
    146     });
     1042  $('print').onclick = printBusinessCards;
     1043
     1044  if ($('downloadPrintPng')) {
     1045    $('downloadPrintPng').onclick = async () => {
     1046      const fBlob = await renderPrintFaceToPngBlob('front', true);
     1047      const bBlob = await renderPrintFaceToPngBlob('back', true);
     1048      if (fBlob) download('awosign-on-yuz-600dpi-kroslu.png', fBlob, 'image/png');
     1049      if (bBlob) download('awosign-arka-yuz-600dpi-kroslu.png', bBlob, 'image/png');
     1050      setStatus(
     1051        val('language') === 'en'
     1052          ? 'Exported 600 DPI Front & Back PNGs with 3 mm bleed & crop marks.'
     1053          : 'Ön ve arka yüz 600 DPI matbaa PNG kalıpları (3 mm taşma + kroslu) indirildi.'
     1054      );
     1055    };
     1056  }
     1057
     1058  if ($('downloadStandardPng')) {
     1059    $('downloadStandardPng').onclick = async () => {
     1060      const fBlob = await renderPrintFaceToPngBlob('front', false);
     1061      const bBlob = await renderPrintFaceToPngBlob('back', false);
     1062      if (fBlob) download('awosign-on-yuz-300ppi.png', fBlob, 'image/png');
     1063      if (bBlob) download('awosign-arka-yuz-300ppi.png', bBlob, 'image/png');
     1064      setStatus(
     1065        val('language') === 'en'
     1066          ? 'Exported 300 PPI Front & Back PNG cards.'
     1067          : 'Ön ve arka yüz 300 PPI PNG kartvizitler indirildi.'
     1068      );
     1069    };
     1070  }
     1071
     1072  if ($('downloadDigitalPng')) {
     1073    $('downloadDigitalPng').onclick = async () => {
     1074      const bBlob = await renderPrintFaceToPngBlob('back', false);
     1075      if (bBlob) download('awosign-dijital-kart.png', bBlob, 'image/png');
     1076      setStatus(
     1077        val('language') === 'en'
     1078          ? 'Digital business card PNG downloaded.'
     1079          : 'Dijital kartvizit PNG olarak indirildi.'
     1080      );
     1081    };
     1082  }
     1083
     1084  $('downloadSig').onclick = () => {
     1085    download(
     1086      'awosign-signature.html',
     1087      '<!doctype html><html><head><meta charset="utf-8"><title>AwoSign signature</title></head><body>' +
     1088        signatureHtml() +
     1089        '</body></html>',
     1090      'text/html;charset=utf-8'
     1091    );
     1092    setStatus(val('language') === 'en' ? 'Signature HTML downloaded.' : 'İmza HTML dosyası indirildi.');
    1471093  };
    148   function signatureHtml() {
    149     const brand = val('brand'),
    150       accent = val('accent');
    151     return '<table role="presentation" cellpadding="0" cellspacing="0" border="0" style="width:600px;max-width:100%;font-family:Arial,Helvetica,sans-serif;color:#15263c;background:#fff"><tr><td style="width:170px;padding:18px;vertical-align:middle">' + logoHtml('max-width:150px;max-height:100px;width:auto;height:auto') + '</td><td style="padding:18px;border-left:2px solid ' + accent + ';font-size:12px;line-height:1.6"><strong style="font-size:19px">' + esc(val('person')) + '</strong><br>' + esc(val('job')) + '<br>' + esc(val('company')) + '<br>' + esc(val('phone')) + '<br>' + esc(val('email')) + '<br>' + esc(val('website')) + '<div style="margin-top:8px">' + socialHtml().replace(/<span>/g, '<span style="display:inline-block;margin:2px;padding:4px;background:' + brand + ';color:white;font-size:11px">') + '</div></td></tr><tr><td colspan="2" style="padding:12px 18px;border-top:1px solid #ddd;font-size:11px">' + esc(val('address')) + '</td></tr></table>';
    152   }
    153   $('downloadSig').onclick = () => download('awosign-signature.html', '<!doctype html><html><head><meta charset="utf-8"><title>AwoSign signature</title></head><body>' + signatureHtml() + '</body></html>', 'text/html;charset=utf-8');
     1094
    1541095  $('copySig').onclick = async () => {
    1551096    try {
    156       await navigator.clipboard.write([new ClipboardItem({
    157         'text/html': new Blob([signatureHtml()], {
    158           type: 'text/html'
    159         }),
    160         'text/plain': new Blob([$('sig').innerText], {
    161           type: 'text/plain'
     1097      await navigator.clipboard.write([
     1098        new ClipboardItem({
     1099          'text/html': new Blob([signatureHtml()], { type: 'text/html' }),
     1100          'text/plain': new Blob([$('sig').innerText], { type: 'text/plain' })
    1621101        })
    163       })]);
    164       alert(val('language') === 'en' ? 'Copied' : 'Kopyalandı');
     1102      ]);
     1103      setStatus(val('language') === 'en' ? 'Signature copied to clipboard.' : 'E-posta imzası panoya kopyalandı.');
    1651104    } catch {
    166       alert(val('language') === 'en' ? 'Clipboard blocked. Use Download HTML.' : 'Kopyalama engellendi. HTML indir seçeneğini kullanın.');
     1105      setStatus(
     1106        val('language') === 'en'
     1107          ? 'Clipboard blocked. Use Download HTML.'
     1108          : 'Kopyalama engellendi. HTML indir seçeneğini kullanın.'
     1109      );
    1671110    }
    1681111  };
     1112
    1691113  $('saveProject').onclick = () => {
    170     const data = {
    171       version: 1,
    172       logo,
    173       fields: {},
    174       social: {}
    175     };
    176     [...fields, 'brand', 'accent', 'language', 'theme'].forEach(k => data.fields[k] = val(k));
    177     platforms.forEach(p => data.social[p[0]] = {
    178       enabled: $('check_' + p[0]).checked,
    179       value: $('soc_' + p[0]).value
    180     });
    181     download('awosign-project.json', JSON.stringify(data, null, 2), 'application/json');
     1114    syncFormToWorkspace();
     1115    download('awosign-workspace.json', JSON.stringify(workspace, null, 2), 'application/json');
     1116    setStatus(val('language') === 'en' ? 'Workspace JSON downloaded.' : 'Tüm profiller JSON olarak indirildi.');
    1821117  };
    183   // Validate imported project fields; accept PNG/JPEG data URLs only.
     1118
    1841119  $('loadProject').onchange = async e => {
    1851120    try {
    … …  
    1871122      if (!f || f.size > 5000000) throw Error('Invalid file');
    1881123      const data = JSON.parse(await f.text());
    189       if (data.version !== 1 || !data.fields) throw Error('Invalid format');
    190       [...fields, 'brand', 'accent', 'language', 'theme'].forEach(k => {
    191         if (typeof data.fields[k] === 'string') {
    192           if (['brand', 'accent'].includes(k) && !/^#[0-9a-f]{6}$/i.test(data.fields[k])) return;
    193           $(k).value = data.fields[k].slice(0, 5000);
    194         }
    195       });
    196       logo = typeof data.logo === 'string' && /^data:image\/(png|jpeg);base64,[a-z0-9+/=]+$/i.test(data.logo) ? data.logo : '';
    197       platforms.forEach(p => {
    198         const v = data.social && data.social[p[0]];
    199         if (v) {
    200           $('check_' + p[0]).checked = v.enabled === true;
    201           $('soc_' + p[0]).value = typeof v.value === 'string' ? v.value.slice(0, 2000) : '';
    202         }
    203       });
    204       $('language').onchange();
    205       $('theme').onchange();
     1124      if ((data.version === 2 || data.version === 3) && Array.isArray(data.profiles) && data.profiles.length) {
     1125        workspace = data;
     1126        workspace.version = 3;
     1127      } else if (data.version === 1 && data.fields) {
     1128        workspace = {
     1129          version: 3,
     1130          activeProfileId: 'prof_1',
     1131          brand: {
     1132            company: data.fields.company || 'Example Studio',
     1133            brand: data.fields.brand || '#172e49',
     1134            accent: data.fields.accent || '#e7b52e',
     1135            website: data.fields.website || '',
     1136            address: data.fields.address || '',
     1137            logo: typeof data.logo === 'string' ? data.logo : '',
     1138            logoUrl: '',
     1139            sigTemplate: 'classic',
     1140            language: data.fields.language || 'tr',
     1141            theme: data.fields.theme || 'auto'
     1142          },
     1143          profiles: [
     1144            {
     1145              id: 'prof_1',
     1146              person: data.fields.person || '',
     1147              job: data.fields.job || '',
     1148              phone: data.fields.phone || '',
     1149              email: data.fields.email || '',
     1150              qrMode: data.fields.qrMode || 'url',
     1151              qrUrl: data.fields.qrUrl || '',
     1152              social: data.social || {}
     1153            }
     1154          ]
     1155        };
     1156      } else {
     1157        throw Error('Invalid format');
     1158      }
     1159      loadWorkspaceIntoForm();
    2061160      render();
     1161      setStatus(val('language') === 'en' ? 'Workspace loaded.' : 'Proje ve personel profilleri yüklendi.');
    2071162    } catch {
    208       alert('Invalid AwoSign JSON / Geçersiz proje dosyası');
     1163      setStatus(val('language') === 'en' ? 'Invalid AwoSign JSON file.' : 'Geçersiz AwoSign proje dosyası.');
    2091164    }
    2101165    e.target.value = '';
    2111166  };
    212   // Associate existing visible labels with their fields.
     1167
    2131168  root.querySelectorAll('input,select,textarea').forEach(el => {
    2141169    const label = el.previousElementSibling;
    2151170    if (label && label.tagName === 'LABEL' && el.id) label.htmlFor = el.id;
    2161171  });
     1172
    2171173  render();
    2181174})();
  • awosign/trunk/awosign.php

    r3783841 r3783861  
    22/**
    33 * Plugin Name: AwoSign
    4  * Description: Free private workspace for email signatures, digital contact cards and two-sided printable business cards.
    5  * Version: 1.0.1
     4 * Description: Free workspace for multi-template email signatures, digital contact cards, multi-staff profiles, [awosign] shortcode, and 600 DPI printable business cards.
     5 * Version: 1.3.0
    66 * Requires at least: 6.4
    77 * Requires PHP: 7.4
    … …  
    1111 * Text Domain: awosign
    1212 */
    13 if ( ! defined( 'ABSPATH' ) ) { exit; }
     13if ( ! defined( 'ABSPATH' ) ) {
     14    exit;
     15}
     16
     17define( 'AWOSIGN_VERSION', '1.3.0' );
     18define( 'AWOSIGN_OPTION_KEY', 'awosign_workspace_data_v1' );
     19
     20/**
     21 * Resolve WordPress Custom Logo or Site Icon URL if available.
     22 */
     23function awosign_get_wp_site_logo_url() {
     24    $custom_logo_id = get_theme_mod( 'custom_logo' );
     25    if ( $custom_logo_id ) {
     26        $url = wp_get_attachment_image_url( $custom_logo_id, 'full' );
     27        if ( $url ) {
     28            return $url;
     29        }
     30    }
     31    $site_icon = get_site_icon_url( 512 );
     32    return $site_icon ? $site_icon : '';
     33}
     34
     35/**
     36 * Default workspace state structure.
     37 */
     38function awosign_get_default_workspace() {
     39    $site_name = get_bloginfo( 'name' );
     40    $site_url  = home_url( '/' );
     41    return array(
     42        'version'         => 3,
     43        'activeProfileId' => 'prof_1',
     44        'brand'           => array(
     45            'company'     => $site_name ? $site_name : 'Example Studio',
     46            'brand'       => '#172e49',
     47            'accent'      => '#e7b52e',
     48            'website'     => $site_url ? $site_url : 'https://example.com',
     49            'address'     => '123 Example Street, Istanbul, TR',
     50            'logo'        => '',
     51            'logoUrl'     => awosign_get_wp_site_logo_url(),
     52            'sigTemplate' => 'classic',
     53            'language'    => 'tr',
     54            'theme'       => 'auto',
     55        ),
     56        'profiles'        => array(
     57            array(
     58                'id'      => 'prof_1',
     59                'person'  => 'Alex Morgan',
     60                'job'     => 'Creative Director',
     61                'phone'   => '+90 555 123 4567',
     62                'email'   => 'alex@example.com',
     63                'qrMode'  => 'url',
     64                'qrUrl'   => '',
     65                'social'  => array(
     66                    'LinkedIn'  => array( 'enabled' => true, 'value' => 'example' ),
     67                    'Instagram' => array( 'enabled' => true, 'value' => 'example' ),
     68                    'Facebook'  => array( 'enabled' => true, 'value' => 'example' ),
     69                ),
     70            ),
     71        ),
     72    );
     73}
     74
     75/**
     76 * Retrieve saved workspace or defaults.
     77 */
     78function awosign_get_workspace() {
     79    $saved = get_option( AWOSIGN_OPTION_KEY, null );
     80    if ( ! is_array( $saved ) || empty( $saved['profiles'] ) || ! is_array( $saved['profiles'] ) ) {
     81        return awosign_get_default_workspace();
     82    }
     83    return $saved;
     84}
     85
     86/**
     87 * Register WP Admin Menu.
     88 */
    1489function awosign_register_menu() {
    15     add_menu_page( 'AwoSign', 'AwoSign', 'manage_options', 'awosign', 'awosign_render_page', 'dashicons-id-alt', 80 );
     90    add_menu_page(
     91        'AwoSign',
     92        'AwoSign',
     93        'manage_options',
     94        'awosign',
     95        'awosign_render_page',
     96        'dashicons-id-alt',
     97        80
     98    );
    1699}
    17100add_action( 'admin_menu', 'awosign_register_menu' );
     101
     102/**
     103 * Enqueue Admin Designer Assets + WP Media Library + Localize Saved Workspace & Nonce.
     104 */
    18105function awosign_enqueue_assets( $hook ) {
    19     if ( 'toplevel_page_awosign' !== $hook || ! current_user_can( 'manage_options' ) ) { return; }
    20     wp_enqueue_style( 'awosign-designer', plugins_url( 'assets/designer.css', __FILE__ ), array(), '1.0.1' );
     106    if ( 'toplevel_page_awosign' !== $hook || ! current_user_can( 'manage_options' ) ) {
     107        return;
     108    }
     109    wp_enqueue_media();
     110    wp_enqueue_style( 'awosign-designer', plugins_url( 'assets/designer.css', __FILE__ ), array(), AWOSIGN_VERSION );
    21111    wp_enqueue_script( 'awosign-qrcode', plugins_url( 'assets/qrcode.js', __FILE__ ), array(), '1.4.4', true );
    22     wp_enqueue_script( 'awosign-designer', plugins_url( 'assets/designer.js', __FILE__ ), array( 'awosign-qrcode' ), '1.0.1', true );
     112    wp_enqueue_script( 'awosign-designer', plugins_url( 'assets/designer.js', __FILE__ ), array( 'awosign-qrcode' ), AWOSIGN_VERSION, true );
     113
     114    wp_localize_script(
     115        'awosign-designer',
     116        'awosignWpConfig',
     117        array(
     118            'ajaxUrl'     => admin_url( 'admin-ajax.php' ),
     119            'nonce'       => wp_create_nonce( 'awosign_save_workspace_nonce' ),
     120            'siteLogoUrl' => esc_url_raw( awosign_get_wp_site_logo_url() ),
     121            'siteName'    => sanitize_text_field( get_bloginfo( 'name' ) ),
     122            'siteUrl'     => esc_url_raw( home_url( '/' ) ),
     123            'workspace'   => awosign_get_workspace(),
     124        )
     125    );
    23126}
    24127add_action( 'admin_enqueue_scripts', 'awosign_enqueue_assets' );
     128
     129/**
     130 * Render Admin Page.
     131 */
    25132function awosign_render_page() {
    26133    if ( ! current_user_can( 'manage_options' ) ) {
    … …  
    29136    require __DIR__ . '/designer-view.php';
    30137}
     138
     139/**
     140 * Sanitize and Save Workspace via AJAX (manage_options + Nonce protected).
     141 */
     142function awosign_ajax_save_workspace() {
     143    if ( ! current_user_can( 'manage_options' ) ) {
     144        wp_send_json_error( array( 'message' => 'Yetkisiz işlem (manage_options gerekli).' ), 403 );
     145    }
     146    check_ajax_referer( 'awosign_save_workspace_nonce', 'nonce' );
     147
     148    $raw_json = isset( $_POST['workspace'] ) ? wp_unslash( $_POST['workspace'] ) : '';
     149    $decoded  = json_decode( $raw_json, true );
     150    if ( ! is_array( $decoded ) || empty( $decoded['profiles'] ) || ! is_array( $decoded['profiles'] ) ) {
     151        wp_send_json_error( array( 'message' => 'Geçersiz veri formatı.' ), 400 );
     152    }
     153
     154    $brand_in     = isset( $decoded['brand'] ) && is_array( $decoded['brand'] ) ? $decoded['brand'] : array();
     155    $brand_color  = isset( $brand_in['brand'] ) && preg_match( '/^#[0-9a-fA-F]{6}$/', $brand_in['brand'] ) ? $brand_in['brand'] : '#172e49';
     156    $accent_color = isset( $brand_in['accent'] ) && preg_match( '/^#[0-9a-fA-F]{6}$/', $brand_in['accent'] ) ? $brand_in['accent'] : '#e7b52e';
     157
     158    $logo_raw   = isset( $brand_in['logo'] ) ? (string) $brand_in['logo'] : '';
     159    $logo_clean = '';
     160    if ( preg_match( '/^data:image\/(png|jpeg|webp|svg\+xml);base64,[a-zA-Z0-9+\/=]+$/', $logo_raw ) && strlen( $logo_raw ) < 4500000 ) {
     161        $logo_clean = $logo_raw;
     162    } elseif ( filter_var( $logo_raw, FILTER_VALIDATE_URL ) ) {
     163        $logo_clean = esc_url_raw( $logo_raw );
     164    }
     165
     166    $sig_tpl = isset( $brand_in['sigTemplate'] ) && in_array( $brand_in['sigTemplate'], array( 'classic', 'compact', 'banner' ), true )
     167        ? $brand_in['sigTemplate']
     168        : 'classic';
     169
     170    $clean_brand = array(
     171        'company'     => sanitize_text_field( isset( $brand_in['company'] ) ? $brand_in['company'] : '' ),
     172        'brand'       => $brand_color,
     173        'accent'      => $accent_color,
     174        'website'     => esc_url_raw( isset( $brand_in['website'] ) ? $brand_in['website'] : '' ),
     175        'address'     => sanitize_textarea_field( isset( $brand_in['address'] ) ? $brand_in['address'] : '' ),
     176        'logo'        => $logo_clean,
     177        'logoUrl'     => esc_url_raw( isset( $brand_in['logoUrl'] ) ? $brand_in['logoUrl'] : '' ),
     178        'sigTemplate' => $sig_tpl,
     179        'language'    => ( isset( $brand_in['language'] ) && 'en' === $brand_in['language'] ) ? 'en' : 'tr',
     180        'theme'       => ( isset( $brand_in['theme'] ) && in_array( $brand_in['theme'], array( 'auto', 'dark', 'light' ), true ) ) ? $brand_in['theme'] : 'auto',
     181    );
     182
     183    $clean_profiles = array();
     184    foreach ( array_slice( $decoded['profiles'], 0, 50 ) as $idx => $p ) {
     185        if ( ! is_array( $p ) ) {
     186            continue;
     187        }
     188        $pid = isset( $p['id'] ) ? preg_replace( '/[^a-zA-Z0-9_-]/', '', (string) $p['id'] ) : '';
     189        if ( ! $pid ) {
     190            $pid = 'prof_' . ( $idx + 1 );
     191        }
     192        $social_in    = isset( $p['social'] ) && is_array( $p['social'] ) ? $p['social'] : array();
     193        $clean_social = array();
     194        foreach ( $social_in as $plat => $sdata ) {
     195            $plat_key = preg_replace( '/[^a-zA-Z0-9]/', '', (string) $plat );
     196            if ( $plat_key && is_array( $sdata ) ) {
     197                $clean_social[ $plat_key ] = array(
     198                    'enabled' => ! empty( $sdata['enabled'] ),
     199                    'value'   => sanitize_text_field( isset( $sdata['value'] ) ? $sdata['value'] : '' ),
     200                );
     201            }
     202        }
     203        $clean_profiles[] = array(
     204            'id'     => $pid,
     205            'person' => sanitize_text_field( isset( $p['person'] ) ? $p['person'] : '' ),
     206            'job'    => sanitize_text_field( isset( $p['job'] ) ? $p['job'] : '' ),
     207            'phone'  => sanitize_text_field( isset( $p['phone'] ) ? $p['phone'] : '' ),
     208            'email'  => sanitize_email( isset( $p['email'] ) ? $p['email'] : '' ),
     209            'qrMode' => ( isset( $p['qrMode'] ) && 'vcard' === $p['qrMode'] ) ? 'vcard' : 'url',
     210            'qrUrl'  => esc_url_raw( isset( $p['qrUrl'] ) ? $p['qrUrl'] : '' ),
     211            'social' => $clean_social,
     212        );
     213    }
     214
     215    if ( empty( $clean_profiles ) ) {
     216        $clean_profiles = awosign_get_default_workspace()['profiles'];
     217    }
     218
     219    $active_id = isset( $decoded['activeProfileId'] ) ? preg_replace( '/[^a-zA-Z0-9_-]/', '', (string) $decoded['activeProfileId'] ) : $clean_profiles[0]['id'];
     220
     221    $to_save = array(
     222        'version'         => 3,
     223        'activeProfileId' => $active_id,
     224        'brand'           => $clean_brand,
     225        'profiles'        => $clean_profiles,
     226        'updatedAt'       => current_time( 'mysql' ),
     227    );
     228
     229    update_option( AWOSIGN_OPTION_KEY, $to_save, false );
     230
     231    wp_send_json_success(
     232        array(
     233            'message'   => 'AwoSign profilleri WordPress veritabanına kaydedildi.',
     234            'workspace' => $to_save,
     235        )
     236    );
     237}
     238add_action( 'wp_ajax_awosign_save_workspace', 'awosign_ajax_save_workspace' );
     239
     240/**
     241 * Frontend Shortcode: [awosign id="prof_1" theme="auto"]
     242 */
     243function awosign_render_shortcode( $atts ) {
     244    $atts = shortcode_atts(
     245        array(
     246            'id'    => '',
     247            'theme' => 'auto',
     248        ),
     249        $atts,
     250        'awosign'
     251    );
     252
     253    $workspace = awosign_get_workspace();
     254    $brand     = isset( $workspace['brand'] ) ? $workspace['brand'] : array();
     255    $profiles  = isset( $workspace['profiles'] ) && is_array( $workspace['profiles'] ) ? $workspace['profiles'] : array();
     256    if ( empty( $profiles ) ) {
     257        return '';
     258    }
     259
     260    $target_id = $atts['id'] ? $atts['id'] : ( isset( $workspace['activeProfileId'] ) ? $workspace['activeProfileId'] : '' );
     261    $profile   = $profiles[0];
     262    foreach ( $profiles as $candidate ) {
     263        if ( isset( $candidate['id'] ) && $candidate['id'] === $target_id ) {
     264            $profile = $candidate;
     265            break;
     266        }
     267    }
     268
     269    wp_enqueue_style( 'awosign-designer', plugins_url( 'assets/designer.css', __FILE__ ), array(), AWOSIGN_VERSION );
     270    wp_enqueue_script( 'awosign-qrcode', plugins_url( 'assets/qrcode.js', __FILE__ ), array(), '1.4.4', true );
     271
     272    $company      = isset( $brand['company'] ) ? $brand['company'] : '';
     273    $brand_color  = isset( $brand['brand'] ) ? $brand['brand'] : '#172e49';
     274    $accent_color = isset( $brand['accent'] ) ? $brand['accent'] : '#e7b52e';
     275    $website      = isset( $brand['website'] ) ? $brand['website'] : '';
     276    $address      = isset( $brand['address'] ) ? $brand['address'] : '';
     277    $logo         = ! empty( $brand['logoUrl'] ) ? $brand['logoUrl'] : ( isset( $brand['logo'] ) ? $brand['logo'] : '' );
     278
     279    $person  = isset( $profile['person'] ) ? $profile['person'] : '';
     280    $job     = isset( $profile['job'] ) ? $profile['job'] : '';
     281    $phone   = isset( $profile['phone'] ) ? $profile['phone'] : '';
     282    $email   = isset( $profile['email'] ) ? $profile['email'] : '';
     283    $qr_mode = isset( $profile['qrMode'] ) ? $profile['qrMode'] : 'url';
     284    $qr_url  = ! empty( $profile['qrUrl'] ) ? $profile['qrUrl'] : $website;
     285
     286    $vcard_lines = array(
     287        'BEGIN:VCARD',
     288        'VERSION:3.0',
     289        'FN:' . $person,
     290        'ORG:' . $company,
     291        'TITLE:' . $job,
     292    );
     293    if ( $phone ) {
     294        $vcard_lines[] = 'TEL;TYPE=WORK,VOICE:' . $phone;
     295    }
     296    if ( $email ) {
     297        $vcard_lines[] = 'EMAIL:' . $email;
     298    }
     299    if ( $website ) {
     300        $vcard_lines[] = 'URL:' . $website;
     301    }
     302    if ( $address ) {
     303        $vcard_lines[] = 'ADR;TYPE=WORK:;;' . str_replace( array( "\r\n", "\r", "\n" ), ' ', $address ) . ';;;;';
     304    }
     305    $vcard_lines[] = 'END:VCARD';
     306    $vcard_payload = implode( "\r\n", $vcard_lines );
     307
     308    $qr_payload = ( 'vcard' === $qr_mode ) ? $vcard_payload : $qr_url;
     309    $uid        = 'awosign-sc-' . wp_unique_id();
     310    $theme_attr = in_array( $atts['theme'], array( 'auto', 'dark', 'light' ), true ) ? $atts['theme'] : 'auto';
     311    $wa_text    = rawurlencode( trim( $person . ' — ' . $job . ' (' . $company . ')' . "\n" . $phone . "\n" . $email . "\n" . $website ) );
     312
     313    ob_start();
     314    ?>
     315    <div id="<?php echo esc_attr( $uid ); ?>" class="awosign-shortcode-card" data-theme="<?php echo esc_attr( $theme_attr ); ?>" style="--brand:<?php echo esc_attr( $brand_color ); ?>;--accent:<?php echo esc_attr( $accent_color ); ?>;max-width:540px;margin:16px auto;font-family:system-ui,-apple-system,Segoe UI,Arial,sans-serif;">
     316        <div class="digital" style="width:100%;background:var(--brand);color:#fff;border-top:5px solid var(--accent);padding:24px;border-radius:12px;display:flex;justify-content:space-between;align-items:center;gap:16px;flex-wrap:wrap;box-shadow:0 6px 24px rgba(0,0,0,0.18);">
     317            <div class="details" style="line-height:1.65;font-size:13px;flex:1;min-width:210px;">
     318                <?php if ( $logo ) : ?>
     319                    <img src="<?php echo esc_url( $logo ); ?>" alt="<?php echo esc_attr( $company ); ?>" style="max-width:110px;max-height:70px;object-fit:contain;display:block;margin-bottom:8px;">
     320                <?php else : ?>
     321                    <div style="font-size:20px;font-weight:700;margin-bottom:6px;"><?php echo esc_html( $company ); ?></div>
     322                <?php endif; ?>
     323                <strong style="font-size:20px;display:block;"><?php echo esc_html( $person ); ?></strong>
     324                <span><?php echo esc_html( $job ); ?><?php echo $company ? ' · ' . esc_html( $company ) : ''; ?></span><br>
     325                <?php if ( $phone ) : ?>
     326                    <a href="tel:<?php echo esc_attr( preg_replace( '/[^0-9+]/', '', $phone ) ); ?>" style="color:#fff;text-decoration:none;"><?php echo esc_html( $phone ); ?></a><br>
     327                <?php endif; ?>
     328                <?php if ( $email ) : ?>
     329                    <a href="mailto:<?php echo esc_attr( $email ); ?>" style="color:#fff;text-decoration:none;"><?php echo esc_html( $email ); ?></a><br>
     330                <?php endif; ?>
     331                <?php if ( $website ) : ?>
     332                    <a href="<?php echo esc_url( $website ); ?>" target="_blank" rel="noopener noreferrer" style="color:#dbe7f5;text-decoration:none;"><?php echo esc_html( $website ); ?></a><br>
     333                <?php endif; ?>
     334                <?php if ( $address ) : ?>
     335                    <span style="font-size:12px;color:#cbd9ea;"><?php echo esc_html( $address ); ?></span>
     336                <?php endif; ?>
     337                <div style="margin-top:12px;display:flex;gap:8px;flex-wrap:wrap;">
     338                    <button type="button" class="awosign-sc-vcf-btn" style="background:var(--accent);color:#102032;border:none;padding:7px 13px;border-radius:6px;font-weight:700;font-size:12px;cursor:pointer;">
     339                        📇 Rehbere Ekle (VCF)
     340                    </button>
     341                    <a href="https://wa.me/?text=<?php echo esc_attr( $wa_text ); ?>" target="_blank" rel="noopener noreferrer" style="background:#25D366;color:#fff;text-decoration:none;padding:7px 13px;border-radius:6px;font-weight:700;font-size:12px;display:inline-block;">
     342                        💬 WhatsApp Paylaş
     343                    </a>
     344                </div>
     345            </div>
     346            <div class="awosign-sc-qr" data-qr="<?php echo esc_attr( $qr_payload ); ?>" style="background:#fff;padding:6px;border-radius:8px;width:108px;height:108px;display:flex;align-items:center;justify-content:center;flex-shrink:0;"></div>
     347        </div>
     348    </div>
     349    <script>
     350    (function(){
     351        var wrap = document.getElementById(<?php echo wp_json_encode( $uid ); ?>);
     352        if (!wrap) return;
     353        var qrBox = wrap.querySelector('.awosign-sc-qr');
     354        if (qrBox && typeof qrcode === 'function') {
     355            try {
     356                var q = qrcode(0, 'L');
     357                q.addData(qrBox.getAttribute('data-qr') || '', 'Byte');
     358                q.make();
     359                qrBox.innerHTML = '<img src="' + q.createDataURL(4, 4) + '" alt="QR" style="width:96px;height:96px;object-fit:contain;">';
     360            } catch (e) {}
     361        }
     362        var btn = wrap.querySelector('.awosign-sc-vcf-btn');
     363        if (btn) {
     364            btn.addEventListener('click', function(){
     365                var vcfData = <?php echo wp_json_encode( $vcard_payload ); ?>;
     366                var blob = new Blob([vcfData], {type: 'text/vcard;charset=utf-8'});
     367                var a = document.createElement('a');
     368                a.href = URL.createObjectURL(blob);
     369                a.download = <?php echo wp_json_encode( sanitize_title( $person ? $person : 'contact' ) . '.vcf' ); ?>;
     370                document.body.appendChild(a);
     371                a.click();
     372                a.remove();
     373            });
     374        }
     375    })();
     376    </script>
     377    <?php
     378    return ob_get_clean();
     379}
     380add_shortcode( 'awosign', 'awosign_render_shortcode' );
  • awosign/trunk/designer-view.php

    r3783841 r3783861  
    22if ( ! defined( 'ABSPATH' ) ) { exit; }
    33?>
    4 <div id="awosign-root"><header><div><h1>AwoSign <span style="color:#e7b52e">Free 1.0.1</span> — Kurumsal Kimlik</h1><small>Tek marka kimliği · E-posta imzası · Dijital kartvizit · Baskı kartviziti · WordPress Free</small></div><div class="two" style="min-width:245px"><div><label for="language">Dil / Language</label><select id="language"><option value="tr">Türkçe</option><option value="en">English</option></select></div><div><label for="theme">Görünüm</label><select id="theme"><option value="dark">Koyu</option><option value="light">Açık</option></select></div></div></header><main><section class="panel editor"><h2 data-tr="Kurumsal kimlik ve iletişim" data-en="Brand identity and contact">Kurumsal kimlik ve iletişim</h2><label data-tr="Şirket logosu (PNG / JPG)" data-en="Company logo (PNG / JPG)">Şirket logosu (PNG / JPG)</label><input id="logoFile" type="file" accept="image/png,image/jpeg"><div class="two"><div><label data-tr="Ana renk" data-en="Primary color">Ana renk</label><input type="color" id="brand" value="#172e49"></div><div><label data-tr="Vurgu rengi" data-en="Accent color">Vurgu rengi</label><input type="color" id="accent" value="#e7b52e"></div></div><label data-tr="Şirket adı" data-en="Company name">Şirket adı</label><input id="company" value="Example Studio"><div class="two"><div><label data-tr="Ad soyad" data-en="Full name">Ad soyad</label><input id="person" value="Alex Morgan"></div><div><label data-tr="Unvan" data-en="Job title">Unvan</label><input id="job" value="Creative Director"></div></div><div class="two"><div><label data-tr="Telefon (ülke koduyla)" data-en="Phone (with country code)">Telefon (ülke koduyla)</label><input type="tel" id="phone" value="+44 555 123 4567"></div><div><label>E-posta / Email</label><input type="email" id="email" value="alex@example.com"></div></div><label data-tr="Web sitesi" data-en="Website">Web sitesi</label><input id="website" value="https://example.com"><label data-tr="Adres" data-en="Address">Adres</label><textarea id="address" rows="2">123 Example Street, London, UK</textarea><label data-tr="QR hedef adresi (boşsa web sitesi)" data-en="QR destination (defaults to website)">QR hedef adresi (boşsa web sitesi)</label><input id="qrUrl" placeholder="https://..."><h3 data-tr="Gösterilecek sosyal medya ikonları" data-en="Social icons to display">Gösterilecek sosyal medya ikonları</h3><div class="social" id="socials"></div><p class="hint" data-tr="Tüm materyaller aynı logo ve renkleri kullanır. Sosyal bağlantılar için platform adresi + kullanıcı adı girilir." data-en="All materials share one logo and color palette. Enter a username or full URL for social profiles.">Tüm materyaller aynı logo ve renkleri kullanır. Sosyal bağlantılar için platform adresi + kullanıcı adı girilir.</p><div id="socialInputs"></div><div class="buttons"><button id="saveProject" data-tr="Proje indir (JSON)" data-en="Download project (JSON)">Proje indir (JSON)</button><label for="loadProject" data-tr="Proje yükle" data-en="Load project">Proje yükle</label><input id="loadProject" type="file" accept="application/json,.json"></div></section><section class="panel"><div class="tabs"><button class="active" data-tab="Signature" data-tr="E-posta imzası" data-en="Email signature">E-posta imzası</button><button data-tab="Digital" data-tr="Dijital kartvizit" data-en="Digital card">Dijital kartvizit</button><button data-tab="Print" data-tr="Baskı kartviziti (iki yüz)" data-en="Print card (two sides)">Baskı kartviziti (iki yüz)</button></div><div id="viewSignature" class="stage"><h3 data-tr="E-posta imzası — 600 px" data-en="Email signature — 600 px">E-posta imzası — 600 px</h3><div id="sig" class="signature"></div><div class="buttons"><button id="copySig" data-tr="İmzayı kopyala" data-en="Copy signature">İmzayı kopyala</button><button id="downloadSig" data-tr="HTML indir" data-en="Download HTML">HTML indir</button></div></div><div id="viewDigital" class="stage hidden"><h3 data-tr="Dijital kartvizit — 540 px" data-en="Digital business card — 540 px">Dijital kartvizit — 540 px</h3><div id="digital" class="digital"></div><div class="buttons"><button id="vcf" data-tr="VCF indir" data-en="Download VCF">VCF indir</button></div></div><div id="viewPrint" class="stage hidden"><h3 data-tr="Baskı kartviziti — ön ve arka yüz" data-en="Print business card — front and back">Baskı kartviziti — ön ve arka yüz</h3><div class="cards"><div class="cardwrap"><h3 data-tr="ÖN YÜZ" data-en="FRONT">ÖN YÜZ</h3><div id="front" class="card front"></div></div><div class="cardwrap"><h3 data-tr="ARKA YÜZ" data-en="BACK">ARKA YÜZ</h3><div id="back" class="card back"></div></div></div><div class="buttons"><button id="print" data-tr="İki yüzü PDF olarak yazdır" data-en="Print both sides to PDF">İki yüzü PDF olarak yazdır</button><button id="vcf2" data-tr="VCF indir" data-en="Download VCF">VCF indir</button></div><p class="hint" data-tr="Baskı prototipi: 85 × 55 mm bitmiş ölçü + 3 mm taşma payı. PDF renk ayrımı, kesim işaretleri ve QR baskı kalitesi henüz matbaa onaylı değildir." data-en="Print prototype: 85 × 55 mm trim + 3 mm bleed. Color separation, crop marks and QR print quality are not press-validated.">Baskı prototipi: 85 × 55 mm bitmiş ölçü + 3 mm taşma payı. PDF renk ayrımı, kesim işaretleri ve QR baskı kalitesi henüz matbaa onaylı değildir.</p></div><p class="muted" data-tr="QR kodları cihazınızda üretilir. Bilgiler WordPress veritabanına kaydedilmez. E-posta istemcileri gömülü logoları engelleyebilir; imzayı kendi istemcinizde test edin." data-en="QR codes are generated on your device. Details are not saved to the WordPress database. Email clients may block embedded logos; test your signature in your email client.">QR kodları cihazınızda üretilir. Bilgiler WordPress veritabanına kaydedilmez. E-posta istemcileri gömülü logoları engelleyebilir; imzayı kendi istemcinizde test edin.</p></section></main><p class="privacy" data-tr="Ücretsiz · Hesap veya API anahtarı gerekmez · Form verileri sunucuya gönderilmez. Dış profil bağlantıları yalnız tıklandığında açılır." data-en="Free · No account or API key required · Form data is not sent to the server. External profile links open only when clicked.">Ücretsiz · Hesap veya API anahtarı gerekmez · Form verileri sunucuya gönderilmez. Dış profil bağlantıları yalnız tıklandığında açılır.</p></div>
     4<div id="awosign-root" data-theme="auto">
     5    <header>
     6        <div>
     7            <h1>AwoSign <span style="color:#e7b52e">Free 1.3.0</span> — Kurumsal Kimlik, Çoklu İmza Şablonu &amp; Matbaa Stüdyosu</h1>
     8            <small data-tr="3 E-posta imza şablonu · Medya Kütüphanesi URL logosu · Çoklu personel · WhatsApp paylaşım · A4 10'lu &amp; 600 DPI matbaa kartviziti" data-en="3 Email signature templates · Media Library URL logo · Multi-staff · WhatsApp share · A4 10-up &amp; 600 DPI print-shop card">3 E-posta imza şablonu · Medya Kütüphanesi URL logosu · Çoklu personel · WhatsApp paylaşım · A4 10'lu &amp; 600 DPI matbaa kartviziti</small>
     9        </div>
     10        <div class="two" style="min-width:290px">
     11            <div>
     12                <label for="language">Dil / Language</label>
     13                <select id="language">
     14                    <option value="tr">Türkçe</option>
     15                    <option value="en">English</option>
     16                </select>
     17            </div>
     18            <div>
     19                <label for="theme" data-tr="Görünüm (Day/Night)" data-en="Appearance (Day/Night)">Görünüm (Day/Night)</label>
     20                <select id="theme">
     21                    <option value="auto" data-tr="◐ Otomatik (Sistem/Sayfa)" data-en="◐ Auto (System/Page)">◐ Otomatik (Sistem/Sayfa)</option>
     22                    <option value="dark" data-tr="☾ Gece (Koyu)" data-en="☾ Night (Dark)">☾ Gece (Koyu)</option>
     23                    <option value="light" data-tr="☀ Gündüz (Açık)" data-en="☀ Day (Light)">☀ Gündüz (Açık)</option>
     24                </select>
     25            </div>
     26        </div>
     27    </header>
     28    <main>
     29        <section class="panel editor">
     30            <div class="as-staff-bar">
     31                <div class="as-staff-header">
     32                    <h2 data-tr="👥 Personel Profilleri &amp; WordPress Kaydı" data-en="👥 Staff Profiles &amp; WordPress Storage">👥 Personel Profilleri &amp; WordPress Kaydı</h2>
     33                    <span class="as-badge" id="profileCountBadge">1 Profil</span>
     34                </div>
     35                <label for="profileSelect" data-tr="Aktif Personel Profili" data-en="Active Staff Profile">Aktif Personel Profili</label>
     36                <select id="profileSelect"></select>
     37                <div class="buttons" style="margin-top:10px">
     38                    <button type="button" id="addProfile" data-tr="➕ Yeni Personel" data-en="➕ New Staff">➕ Yeni Personel</button>
     39                    <button type="button" id="deleteProfile" data-tr="🗑️ Profili Sil" data-en="🗑️ Delete Profile">🗑️ Profili Sil</button>
     40                    <button type="button" id="saveWpDb" class="as-btn-primary" data-tr="💾 WordPress'e Kaydet" data-en="💾 Save to WordPress">💾 WordPress'e Kaydet</button>
     41                </div>
     42                <div class="as-shortcode-box">
     43                    <label for="shortcodeDisplay" data-tr="Sayfa / Yazı Kısa Kodu (Shortcode)" data-en="Page / Post Shortcode">Sayfa / Yazı Kısa Kodu (Shortcode)</label>
     44                    <div class="as-shortcode-row">
     45                        <input id="shortcodeDisplay" readonly value='[awosign id="prof_1"]'>
     46                        <button type="button" id="copyShortcode" data-tr="📋 Kopyala" data-en="📋 Copy">📋 Kopyala</button>
     47                    </div>
     48                </div>
     49            </div>
     50
     51            <h2 data-tr="🏢 Ortak Kurumsal Kimlik (Tüm Personeller)" data-en="🏢 Shared Brand Identity (All Staff)" style="margin-top:18px">🏢 Ortak Kurumsal Kimlik (Tüm Personeller)</h2>
     52            <label data-tr="Şirket logosu dosyası (Baskı / PNG için)" data-en="Company logo file (For Print / PNG)">Şirket logosu dosyası (Baskı / PNG için)</label>
     53            <div class="as-logo-row">
     54                <input id="logoFile" type="file" accept="image/png,image/jpeg">
     55                <button type="button" id="useSiteLogo" data-tr="🌐 Site Logosunu Çek" data-en="🌐 Use Site Logo">🌐 Site Logosunu Çek</button>
     56            </div>
     57            <label for="logoUrl" data-tr="E-posta imzası için Logo URL (Gmail/Outlook Base64 engeline takılmaz)" data-en="Logo URL for Email Signatures (Avoids Gmail/Outlook Base64 block)">E-posta imzası için Logo URL (Gmail/Outlook Base64 engeline takılmaz)</label>
     58            <div class="as-logo-row">
     59                <input id="logoUrl" placeholder="https://siteniz.com/wp-content/uploads/logo.png">
     60                <button type="button" id="pickWpMediaLogo" data-tr="🖼️ Medya Seç" data-en="🖼️ Media Library">🖼️ Medya Seç</button>
     61            </div>
     62            <div class="two">
     63                <div>
     64                    <label data-tr="Ana renk" data-en="Primary color">Ana renk</label>
     65                    <input type="color" id="brand" value="#172e49">
     66                </div>
     67                <div>
     68                    <label data-tr="Vurgu rengi" data-en="Accent color">Vurgu rengi</label>
     69                    <input type="color" id="accent" value="#e7b52e">
     70                </div>
     71            </div>
     72            <label data-tr="Şirket adı" data-en="Company name">Şirket adı</label>
     73            <input id="company" value="Example Studio">
     74            <label data-tr="Web sitesi" data-en="Website">Web sitesi</label>
     75            <input id="website" value="https://example.com">
     76            <label data-tr="Adres" data-en="Address">Adres</label>
     77            <textarea id="address" rows="2">123 Example Street, Istanbul, TR</textarea>
     78
     79            <h2 data-tr="👤 Seçili Personel Bilgileri" data-en="👤 Selected Staff Details" style="margin-top:18px">👤 Seçili Personel Bilgileri</h2>
     80            <div class="two">
     81                <div>
     82                    <label data-tr="Ad soyad" data-en="Full name">Ad soyad</label>
     83                    <input id="person" value="Alex Morgan">
     84                </div>
     85                <div>
     86                    <label data-tr="Unvan" data-en="Job title">Unvan</label>
     87                    <input id="job" value="Creative Director">
     88                </div>
     89            </div>
     90            <div class="two">
     91                <div>
     92                    <label data-tr="Telefon (ülke koduyla)" data-en="Phone (with country code)">Telefon (ülke koduyla)</label>
     93                    <input type="tel" id="phone" value="+90 555 123 4567">
     94                </div>
     95                <div>
     96                    <label>E-posta / Email</label>
     97                    <input type="email" id="email" value="alex@example.com">
     98                </div>
     99            </div>
     100            <div class="two">
     101                <div>
     102                    <label for="qrMode" data-tr="QR Kod İçeriği" data-en="QR Code Content">QR Kod İçeriği</label>
     103                    <select id="qrMode">
     104                        <option value="url" data-tr="Web sitesi / Özel URL" data-en="Website / Custom URL">Web sitesi / Özel URL</option>
     105                        <option value="vcard" data-tr="vCard Rehber (Kamerayla Rehbere Ekle)" data-en="vCard Contact (Scan to Save)">vCard Rehber (Kamerayla Rehbere Ekle)</option>
     106                    </select>
     107                </div>
     108                <div>
     109                    <label data-tr="QR hedef URL (boşsa web sitesi)" data-en="QR destination (defaults to website)">QR hedef URL (boşsa web sitesi)</label>
     110                    <input id="qrUrl" placeholder="https://...">
     111                </div>
     112            </div>
     113            <h3 data-tr="Gösterilecek sosyal medya ikonları" data-en="Social icons to display">Gösterilecek sosyal medya ikonları</h3>
     114            <div class="social" id="socials"></div>
     115            <p class="hint" data-tr="Tüm personeller aynı şirket logosu ve renklerini paylaşır. Sosyal bağlantılar seçili personele özeldir." data-en="All staff members share the company logo and color palette. Social links belong to the selected staff profile.">Tüm personeller aynı şirket logosu ve renklerini paylaşır. Sosyal bağlantılar seçili personele özeldir.</p>
     116            <div id="socialInputs"></div>
     117            <div class="buttons">
     118                <button type="button" id="saveProject" data-tr="📦 Tüm Profilleri İndir (JSON)" data-en="📦 Export All Profiles (JSON)">📦 Tüm Profilleri İndir (JSON)</button>
     119                <label for="loadProject" class="as-file-btn" data-tr="📂 JSON Yükle" data-en="📂 Import JSON">📂 JSON Yükle</label>
     120                <input id="loadProject" type="file" accept="application/json,.json" class="hidden">
     121            </div>
     122        </section>
     123        <section class="panel">
     124            <div class="tabs">
     125                <button class="active" data-tab="Signature" data-tr="E-posta imzası" data-en="Email signature">E-posta imzası</button>
     126                <button data-tab="Digital" data-tr="Dijital kartvizit" data-en="Digital card">Dijital kartvizit</button>
     127                <button data-tab="Print" data-tr="Baskı &amp; Çoğaltma (İki Yüz)" data-en="Print &amp; Duplication (Two Sides)">Baskı &amp; Çoğaltma (İki Yüz)</button>
     128            </div>
     129            <div id="viewSignature" class="stage">
     130                <div style="display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:wrap;margin-bottom:14px;">
     131                    <h3 style="margin:0" data-tr="E-posta imzası — 600 px" data-en="Email signature — 600 px">E-posta imzası — 600 px</h3>
     132                    <div style="min-width:250px;">
     133                        <select id="sigTemplate" aria-label="E-posta imza şablonu">
     134                            <option value="classic" data-tr="Şablon 1: Klasik Sol Logo (600 px)" data-en="Template 1: Classic Left Logo (600 px)">Şablon 1: Klasik Sol Logo (600 px)</option>
     135                            <option value="compact" data-tr="Şablon 2: Modern Kompakt Şerit (520 px)" data-en="Template 2: Modern Compact Bar (520 px)">Şablon 2: Modern Kompakt Şerit (520 px)</option>
     136                            <option value="banner" data-tr="Şablon 3: Kurumsal Başlık &amp; QR (600 px)" data-en="Template 3: Corporate Banner &amp; QR (600 px)">Şablon 3: Kurumsal Başlık &amp; QR (600 px)</option>
     137                        </select>
     138                    </div>
     139                </div>
     140                <div id="sig" class="signature"></div>
     141                <div class="buttons">
     142                    <button id="copySig" data-tr="📋 İmzayı Kopyala" data-en="📋 Copy Signature">📋 İmzayı Kopyala</button>
     143                    <button id="downloadSig" data-tr="📄 HTML İndir" data-en="📄 Download HTML">📄 HTML İndir</button>
     144                </div>
     145            </div>
     146            <div id="viewDigital" class="stage hidden">
     147                <h3 data-tr="Dijital kartvizit — 540 px" data-en="Digital business card — 540 px">Dijital kartvizit — 540 px</h3>
     148                <div id="digital" class="digital"></div>
     149                <div class="buttons">
     150                    <button id="downloadDigitalPng" data-tr="🖼️ Dijital Kartvizit PNG İndir" data-en="🖼️ Download Digital Card PNG">🖼️ Dijital Kartvizit PNG İndir</button>
     151                    <button id="vcf" class="btn-secondary" data-tr="📇 VCF Rehber İndir" data-en="📇 Download VCF">📇 VCF Rehber İndir</button>
     152                    <button id="shareWhatsapp" class="as-btn-primary" data-tr="💬 WhatsApp ile Paylaş" data-en="💬 Share via WhatsApp">💬 WhatsApp ile Paylaş</button>
     153                </div>
     154            </div>
     155            <div id="viewPrint" class="stage hidden">
     156                <h3 data-tr="Baskı kartviziti — 85 × 55 mm ön ve arka yüz" data-en="Print business card — 85 × 55 mm front and back">Baskı kartviziti — 85 × 55 mm ön ve arka yüz</h3>
     157                <div class="cards">
     158                    <div class="cardwrap">
     159                        <h3 data-tr="ÖN YÜZ (85 × 55 mm)" data-en="FRONT (85 × 55 mm)">ÖN YÜZ (85 × 55 mm)</h3>
     160                        <div id="front" class="card front"></div>
     161                    </div>
     162                    <div class="cardwrap">
     163                        <h3 data-tr="ARKA YÜZ (85 × 55 mm)" data-en="BACK (85 × 55 mm)">ARKA YÜZ (85 × 55 mm)</h3>
     164                        <div id="back" class="card back"></div>
     165                    </div>
     166                </div>
     167                <div class="as-print-panel">
     168                    <h4 data-tr="🖨️ Matbaa Baskı &amp; Çoklu Çoğaltma Ayarları" data-en="🖨️ Print-Shop &amp; Bulk Duplication Settings">🖨️ Matbaa Baskı &amp; Çoklu Çoğaltma Ayarları</h4>
     169                    <div class="as-print-grid">
     170                        <div>
     171                            <label for="printLayout" data-tr="Sayfa Düzeni / Çoğaltma Modu" data-en="Page Layout / Duplication Mode">Sayfa Düzeni / Çoğaltma Modu</label>
     172                            <select id="printLayout">
     173                                <option value="a4_10up_duplex" data-tr="A4 Çift Yön Çoklu (2×5 = 10 Kart/Sayfa Ön+Arka)" data-en="A4 Duplex Sheet (2×5 = 10 Cards/Sheet Front+Back)">A4 Çift Yön Çoklu (2×5 = 10 Kart/Sayfa Ön+Arka)</option>
     174                                <option value="a4_10up_front" data-tr="A4 Tek Yön Çoklu — Sadece Ön Yüz (10 Kart/Sayfa)" data-en="A4 Single Side — Front Only (10 Cards/Sheet)">A4 Tek Yön Çoklu — Sadece Ön Yüz (10 Kart/Sayfa)</option>
     175                                <option value="a4_10up_back" data-tr="A4 Tek Yön Çoklu — Sadece Arka Yüz (10 Kart/Sayfa)" data-en="A4 Single Side — Back Only (10 Cards/Sheet)">A4 Tek Yön Çoklu — Sadece Arka Yüz (10 Kart/Sayfa)</option>
     176                                <option value="single_bleed" data-tr="Matbaa Tekli Kalıp (91×61 mm — 3 mm Taşma Paylı + Kroslu)" data-en="Print-Shop Single Master (91×61 mm — 3 mm Bleed + Crop Marks)">Matbaa Tekli Kalıp (91×61 mm — 3 mm Taşma Paylı + Kroslu)</option>
     177                            </select>
     178                        </div>
     179                        <div>
     180                            <label for="printCopies" data-tr="Çoğaltma Adedi (A4 Set Sayısı)" data-en="Copy Multiplier (A4 Sheet Sets)">Çoğaltma Adedi (A4 Set Sayısı)</label>
     181                            <select id="printCopies">
     182                                <option value="1" data-tr="1 Set (10 Adet Kartvizit)" data-en="1 Set (10 Business Cards)">1 Set (10 Adet Kartvizit)</option>
     183                                <option value="2" data-tr="2 Set (20 Adet Kartvizit)" data-en="2 Sets (20 Business Cards)">2 Set (20 Adet Kartvizit)</option>
     184                                <option value="5" data-tr="5 Set (50 Adet Kartvizit)" data-en="5 Sets (50 Business Cards)">5 Set (50 Adet Kartvizit)</option>
     185                                <option value="10" data-tr="10 Set (100 Adet Kartvizit)" data-en="10 Sets (100 Business Cards)">10 Set (100 Adet Kartvizit)</option>
     186                            </select>
     187                        </div>
     188                        <div>
     189                            <label for="cropMarks" data-tr="Kesim Çizgileri (Kros)" data-en="Crop Marks / Cut Guides">Kesim Çizgileri (Kros)</label>
     190                            <select id="cropMarks">
     191                                <option value="1" data-tr="Göster (Giyotin / Maket Bıçağı Kılavuzu)" data-en="Show (Guillotine / Cut Guides)">Göster (Giyotin / Maket Bıçağı Kılavuzu)</option>
     192                                <option value="0" data-tr="Gizle (Tam Sıfır Kenar)" data-en="Hide (Zero Border)">Gizle (Tam Sıfır Kenar)</option>
     193                            </select>
     194                        </div>
     195                    </div>
     196                    <div class="buttons">
     197                        <button id="print" data-tr="🖨️ Seçili Düzeni Yazdır / PDF Kaydet" data-en="🖨️ Print Selected Layout / Save PDF">🖨️ Seçili Düzeni Yazdır / PDF Kaydet</button>
     198                        <button id="downloadPrintPng" class="btn-secondary" data-tr="📐 600 DPI Matbaa PNG (Ön+Arka Kroslu)" data-en="📐 600 DPI Print-Shop PNG (Front+Back w/ Bleed)">📐 600 DPI Matbaa PNG (Ön+Arka Kroslu)</button>
     199                        <button id="downloadStandardPng" class="btn-secondary" data-tr="🖼️ 300 PPI Standart PNG (Ön+Arka)" data-en="🖼️ 300 PPI Standard PNG (Front+Back)">🖼️ 300 PPI Standart PNG (Ön+Arka)</button>
     200                        <button id="vcf2" class="btn-secondary" data-tr="📇 VCF İndir" data-en="📇 Download VCF">📇 VCF İndir</button>
     201                    </div>
     202                </div>
     203                <p class="hint" data-tr="Baskı İpucu: Yazdırma penceresinde 'Kenar Boşlukları: Yok / Sıfır', 'Ölçek: %100' ve 'Arka Plan Grafikleri: Açık' seçin. Çift yönlü A4 baskıda yazıcınızın 'Uzun Kenardan Çevir (Flip on Long Edge)' ayarını kullanın." data-en="Print Tip: Select 'Margins: None', 'Scale: 100%', and 'Background graphics: On' in the print dialog. For duplex A4 printing, use 'Flip on Long Edge'.">Baskı İpucu: Yazdırma penceresinde 'Kenar Boşlukları: Yok / Sıfır', 'Ölçek: %100' ve 'Arka Plan Grafikleri: Açık' seçin. Çift yönlü A4 baskıda yazıcınızın 'Uzun Kenardan Çevir (Flip on Long Edge)' ayarını kullanın.</p>
     204            </div>
     205            <p id="as-status" class="as-status" role="status" aria-live="polite"></p>
     206            <p class="muted" data-tr="QR kodları cihazınızda üretilir. E-posta imzalarında Gmail/Outlook uyumluluğu için 'Logo URL' alanına medya kütüphanesi adresinizi ekleyebilirsiniz." data-en="QR codes are generated on your device. For Gmail/Outlook signature compatibility, provide a public Media Library URL in the 'Logo URL' field.">QR kodları cihazınızda üretilir. E-posta imzalarında Gmail/Outlook uyumluluğu için 'Logo URL' alanına medya kütüphanesi adresinizi ekleyebilirsiniz.</p>
     207        </section>
     208    </main>
     209    <p class="privacy" data-tr="Ücretsiz · Hesap veya API anahtarı gerekmez · Veriler yalnız sizin WordPress sitenizde veya indirdiğiniz JSON dosyasında tutulur." data-en="Free · No account or API key required · Data is stored only on your own WordPress site or exported JSON file.">Ücretsiz · Hesap veya API anahtarı gerekmez · Veriler yalnız sizin WordPress sitenizde veya indirdiğiniz JSON dosyasında tutulur.</p>
     210</div>
  • awosign/trunk/readme.txt

    r3783841 r3783861  
    1 === AwoSign ===
     1=== AwoSign — Email Signature, Digital Contact Card & Printable Business Card Studio ===
    22Contributors: awoscan
     3Tags: email signature, business card, vcard, qr code, digital card
     4Requires at least: 6.4
    35Tested up to: 6.7
    4 Tags: email signature, business card, branding, vcard, qr code
    5 Requires at least: 6.4
    66Requires PHP: 7.4
    7 Stable tag: 1.0.1
     7Stable tag: 1.3.0
    88License: GPLv2 or later
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
    1010
    11 Free brand identity workspace: email signatures, digital card previews, printable business cards and vCard downloads.
     11Multi-template HTML email signature generator, digital contact card with `[awosign]` shortcode, local vCard/URL QR code, and 600 DPI printable business card studio.
    1212
    1313== Description ==
    14 AwoSign turns a shared logo and color palette into an email signature, digital contact card preview and two-sided business card. Free, no account, subscription, trial limit or API key.
     14**AwoSign** turns your company logo and brand palette into a professional **HTML Email Signature**, an interactive **Digital Contact Card (`[awosign]`)**, and a **Two-Sided 85x55mm Printable Business Card** directly inside WordPress.
    1515
    16 * Turkish and English editor; light and dark themes.
    17 * PNG/JPEG logos up to 3 MB.
    18 * Ten selectable social profile links with compact text/symbol badges.
    19 * Local QR generation; no third-party QR service.
    20 * Table-based, inline-styled signature HTML export and rich clipboard copy when supported.
    21 * vCard 3.0 export and two-page browser print/PDF output.
    22 * Explicit JSON project download/import; no automatic persistence.
     16100% free, self-contained, and privacy-first: no external QR APIs, no tracking, and no account required.
    2317
    24 Digital cards are private previews, not published public profile pages. Social badges are text/symbol labels, not official brand pictograms. Print output is RGB browser output: 85 x 55 mm trim plus 3 mm bleed. It does not include crop marks or certified press/color separation. Email compatibility varies; test exported signatures, especially embedded logo images, in your email clients.
     18= Key Features (English) =
     19* **3 HTML Email Signature Templates:** Switch instantly between *Classic (Left Logo)*, *Compact (Horizontal Bar)*, and *Corporate Banner (with QR Code)* — built with inline table styles for Gmail, Outlook, Apple Mail, and Thunderbird.
     20* **Multi-Staff Profile Management:** Manage up to 50 team members under a shared company brand identity and save directly to the WordPress database (`wp_options`) or export/import portable JSON backups.
     21* **`[awosign]` Frontend Shortcode:** Embed interactive digital business cards on any page or post (`[awosign]` or `[awosign id="prof_1"]`) complete with live QR code, one-click **vCard 3.0 (`.vcf`)** download, and **WhatsApp Share**.
     22* **Dual QR Code Engine (100% Offline):** Generate QR codes locally for either a **Website/Custom URL** or a full **vCard 3.0 Contact** (scan with phone camera to add directly to contacts).
     23* **600 DPI Print-Shop PNG & A4 10-Up Print Engine:** Print single 85x55mm cards with 3mm bleed and corner crop marks, or print **A4 10-Up (2x5) front/back duplex sheets** (`10` to `100` cards) and export **600 DPI (`2150x1441 px`)** print-ready PNGs.
     24* **Bilingual & Adaptive Theme:** Full **Turkish & English** interface with Automatic (System/Page), Light, and Dark mode support.
     25
     26= Başlıca Özellikler (Türkçe) =
     27* **3 Kurumsal E-Posta İmzası Şablonu:** Klasik Sol Logo, Kompakt Yatay Bar ve QR Kodlu Kurumsal Banner şablonları (Gmail, Outlook, Apple Mail ve Thunderbird uyumlu tablo tabanlı HTML).
     28* **Çoklu Personel Profili & WordPress Veritabanı Kaydı:** Tek kurumsal kimlik altında tüm ekip profillerini yönetin, WordPress Medya Kütüphanesinden logo seçin.
     29* **`[awosign]` Ön Yüz Kısa Kodu:** Dijital kartvizitinizi QR kod, tek tıkla Rehbere Ekle (`.vcf`) ve WhatsApp paylaşım butonuyla sitenizin istediğiniz sayfasına yerleştirin.
     30* **600 DPI Matbaa Baskı & A4 10'lu Kartvizit Dizgisi:** 85x55mm + 3mm taşma payı (bleed) ve kesim kroslarıyla tekli veya A4 sayfasına 10'lu (10–100 adet) çift yönlü kartvizit baskısı alın.
    2531
    2632== Installation ==
    27 1. Upload the awosign folder to /wp-content/plugins/ or upload the ZIP through Plugins > Add New > Upload Plugin.
    28 2. Activate AwoSign.
    29 3. Administrators: open AwoSign in the dashboard menu.
    30 4. Customize the fields and export. Download a project JSON to keep your work before closing or reloading the page.
    31 
    32 == Privacy ==
    33 No form values or logos are sent to WordPress or third-party services by the plugin. No telemetry, cookies, database options, REST endpoints or external asset CDNs. Editing state is lost on reload unless exported. Project JSON includes contact information and embedded logos; keep it private. Clicking a social link visits that provider. Exported documents may include those links. The plugin only loads assets on its own admin page and requires manage_options. No public shortcode is registered.
     331. Upload the `awosign` folder to `/wp-content/plugins/` or install directly via **Plugins > Add New** in WordPress.
     342. Activate **AwoSign**.
     353. Open **AwoSign** in the WordPress admin sidebar to design signatures and business cards, or place `[awosign]` on any page.
    3436
    3537== Frequently Asked Questions ==
    36 = Does it save company data in WordPress? =
    37 No. Download/import a project JSON file for portable storage.
    38 = Does it require a QR service? =
    39 No. QR Code Generator by Kazuhiko Arase is bundled locally under the MIT license. Source: https://github.com/kazuhikoarase/qrcode-generator (JavaScript 1.4.4).
    40 = Does it include invoice, accounting or stationery functions? =
    41 No. This release follows the supplied signature/card design. It does not include e-invoicing, invoice cards, expense OCR, letterhead or envelopes.
     38= Does AwoSign send any data to external servers or third-party QR APIs? =
     39No. All QR codes, vCard files, HTML signatures, and 600 DPI PNG business cards are generated 100% locally inside your browser and WordPress site.
     40
     41= How do I display a digital business card on a public WordPress page? =
     42Save your workspace in **AwoSign** and add the `[awosign]` shortcode (or `[awosign id="prof_1"]` for a specific team member) to any page, post, or widget.
     43
     44= Why does Gmail or Outlook block Base64 logos in email signatures? =
     45Email clients often block inline `data:image/...` strings. Use the **WordPress Media Library Logo Picker** (`#logoUrl`) in AwoSign so your email signature uses a standard `https://...` image URL.
     46
     47== Screenshots ==
     481. Multi-Template HTML Email Signature Studio (Classic, Compact & Banner with QR Code + Media Library Logo Picker).
     492. Interactive Digital Contact Card & `[awosign]` Frontend Shortcode with vCard (.VCF) download and WhatsApp sharing.
     503. 600 DPI Print-Shop PNG Exporter & A4 10-Up Duplex Printable Business Card Studio (85x55mm + 3mm bleed & crop marks).
    4251
    4352== Source code ==
    44 The plugin includes the readable JavaScript and CSS source files that it runs:
    45 
    46 * assets/designer.js - AwoSign editor, previews, local exports and project import.
    47 * assets/designer.css - Editor and preview styles, scoped to the admin workspace.
    48 * assets/qrcode.js - Readable QR Code Generator 1.4.4 source by Kazuhiko Arase (MIT). Upstream: https://github.com/kazuhikoarase/qrcode-generator
    49 
    50 No build step, package manager, transpilation or minification is required. Edit these files directly; WordPress loads them as shipped. The QR library license is included in assets/qrcode-LICENSE.txt; AwoSign is GPL-2.0-or-later.
     53The plugin ships with human-readable JavaScript and CSS directly (no build or minification step required):
     54* `assets/designer.js` — AwoSign studio, live previews, QR/vCard/PNG exporters, and AJAX persistence.
     55* `assets/designer.css` — Scoped studio and print layout styles.
     56* `assets/qrcode.js` — QR Code Generator 1.4.4 by Kazuhiko Arase (MIT License, see `assets/qrcode-LICENSE.txt`).
    5157
    5258== Changelog ==
     59= 1.3.0 =
     60* Added 3 email signature layout templates (`classic` Left Logo, `compact` Horizontal Bar, and `banner` Corporate Header with QR).
     61* Added WordPress Media Library Logo URL support (`#logoUrl` + `wp.media` picker) for full Gmail/Outlook compatibility.
     62* Added one-click WhatsApp card sharing button in the digital card view and `[awosign]` frontend shortcode.
     63
     64= 1.2.0 =
     65* Added multi-staff profile management under a shared company brand identity.
     66* Added WordPress database storage (`wp_options` via `awosign_workspace_data_v1`) protected by `manage_options` capability and AJAX nonce, plus `uninstall.php` cleanup.
     67* Added `[awosign]` frontend shortcode to embed interactive digital business cards with QR and VCF download.
     68
     69= 1.1.0 =
     70* Added popup-free in-page `@media print` engine (`#as-print-host`) with A4 10-up (2x5) front/back duplex duplication and 600 DPI Print-Shop PNG export (2150x1441 px with 3 mm bleed and corner crop marks).
     71* Added QR Code mode selector: Website/Custom URL or vCard 3.0 Contact QR.
     72* Added Automatic (System/Page Day-Night), Light, and Dark theme selector.
     73
    5374= 1.0.1 =
    54 Provide readable designer JavaScript and CSS directly and document the bundled source files.
     75* Provide readable designer JavaScript and CSS directly and document the bundled source files.
    5576
    5677= 1.0.0 =
    57 Initial WordPress adaptation of the AwoSign V2.2 HTML design. Local QR, scoped assets, table signature export, project files and private admin access.
    58 
    59 == Third-party licenses ==
    60 assets/qrcode.js: QR Code Generator, Copyright (c) 2009 Kazuhiko Arase; MIT license, see assets/qrcode-LICENSE.txt. Other AwoSign code: GPL-2.0-or-later.
    61 
     78* Initial WordPress release of the AwoSign studio.
Note: See TracChangeset for help on using the changeset viewer.