@forgecharts/sdk 1.1.29 → 1.1.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6943,6 +6943,924 @@ var IndicatorDAG = class {
6943
6943
  }
6944
6944
  };
6945
6945
 
6946
+ // src/pine/pine-lexer.ts
6947
+ var PineLexer = class {
6948
+ _src;
6949
+ _pos = 0;
6950
+ _line = 1;
6951
+ _col = 1;
6952
+ /** Version extracted from `//@version=N` */
6953
+ version = 5;
6954
+ constructor(src) {
6955
+ this._src = src.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
6956
+ }
6957
+ tokenize() {
6958
+ const raw = this._rawTokenize();
6959
+ return this._injectIndents(raw);
6960
+ }
6961
+ // ─── Raw tokenisation ────────────────────────────────────────────────────────
6962
+ _rawTokenize() {
6963
+ const tokens = [];
6964
+ while (this._pos < this._src.length) {
6965
+ const ch = this._src[this._pos];
6966
+ if (ch === "\n") {
6967
+ tokens.push(this._make("NEWLINE", "\n"));
6968
+ this._pos++;
6969
+ this._line++;
6970
+ this._col = 1;
6971
+ continue;
6972
+ }
6973
+ if (ch === " " || ch === " ") {
6974
+ this._pos++;
6975
+ this._col++;
6976
+ continue;
6977
+ }
6978
+ if (ch === "/" && this._peek(1) === "/") {
6979
+ const commentStart = this._pos;
6980
+ while (this._pos < this._src.length && this._src[this._pos] !== "\n") {
6981
+ this._pos++;
6982
+ this._col++;
6983
+ }
6984
+ const comment = this._src.slice(commentStart, this._pos);
6985
+ const vMatch = comment.match(/\/\/@version\s*=\s*(\d+)/);
6986
+ if (vMatch) this.version = parseInt(vMatch[1], 10);
6987
+ continue;
6988
+ }
6989
+ if (ch >= "0" && ch <= "9" || ch === "." && this._isDigit(this._peek(1))) {
6990
+ tokens.push(this._readNumber());
6991
+ continue;
6992
+ }
6993
+ if (ch === '"' || ch === "'") {
6994
+ tokens.push(this._readString(ch));
6995
+ continue;
6996
+ }
6997
+ if (this._isAlpha(ch)) {
6998
+ const tok = this._readIdent();
6999
+ tokens.push(tok);
7000
+ continue;
7001
+ }
7002
+ const two = this._src.slice(this._pos, this._pos + 2);
7003
+ if (two === "<=") {
7004
+ tokens.push(this._make("LTE", two));
7005
+ this._adv(2);
7006
+ continue;
7007
+ }
7008
+ if (two === ">=") {
7009
+ tokens.push(this._make("GTE", two));
7010
+ this._adv(2);
7011
+ continue;
7012
+ }
7013
+ if (two === "==") {
7014
+ tokens.push(this._make("EQEQ", two));
7015
+ this._adv(2);
7016
+ continue;
7017
+ }
7018
+ if (two === "!=") {
7019
+ tokens.push(this._make("NEQ", two));
7020
+ this._adv(2);
7021
+ continue;
7022
+ }
7023
+ if (two === ":=") {
7024
+ tokens.push(this._make("REASSIGN", two));
7025
+ this._adv(2);
7026
+ continue;
7027
+ }
7028
+ if (two === "=>") {
7029
+ tokens.push(this._make("ARROW", two));
7030
+ this._adv(2);
7031
+ continue;
7032
+ }
7033
+ const singleOps = {
7034
+ "+": "PLUS",
7035
+ "-": "MINUS",
7036
+ "*": "STAR",
7037
+ "/": "SLASH",
7038
+ "%": "PERCENT",
7039
+ "<": "LT",
7040
+ ">": "GT",
7041
+ "?": "QMARK",
7042
+ ":": "COLON",
7043
+ "(": "LPAREN",
7044
+ ")": "RPAREN",
7045
+ "[": "LBRACKET",
7046
+ "]": "RBRACKET",
7047
+ ",": "COMMA",
7048
+ ".": "DOT",
7049
+ "=": "ASSIGN"
7050
+ };
7051
+ if (ch in singleOps) {
7052
+ tokens.push(this._make(singleOps[ch], ch));
7053
+ this._pos++;
7054
+ this._col++;
7055
+ continue;
7056
+ }
7057
+ throw new SyntaxError(`[Pine] Unexpected character '${ch}' at ${this._line}:${this._col}`);
7058
+ }
7059
+ tokens.push(this._make("EOF", ""));
7060
+ return tokens;
7061
+ }
7062
+ // ─── Indent/Dedent injection ─────────────────────────────────────────────────
7063
+ _injectIndents(raw) {
7064
+ const out = [];
7065
+ const indentStack = [0];
7066
+ let i = 0;
7067
+ while (i < raw.length) {
7068
+ const tok = raw[i];
7069
+ if (tok.kind !== "NEWLINE") {
7070
+ out.push(tok);
7071
+ i++;
7072
+ continue;
7073
+ }
7074
+ out.push(tok);
7075
+ i++;
7076
+ let spaces = 0;
7077
+ let j = i;
7078
+ while (j < raw.length && raw[j].kind === "NEWLINE") {
7079
+ out.push(raw[j]);
7080
+ j++;
7081
+ }
7082
+ i = j;
7083
+ if (i >= raw.length || raw[i].kind === "EOF") break;
7084
+ const nextTok = raw[i];
7085
+ spaces = nextTok.col - 1;
7086
+ const currentIndent = indentStack[indentStack.length - 1];
7087
+ if (spaces > currentIndent) {
7088
+ indentStack.push(spaces);
7089
+ out.push({ kind: "INDENT", value: "", line: nextTok.line, col: 1 });
7090
+ } else {
7091
+ while (spaces < indentStack[indentStack.length - 1]) {
7092
+ indentStack.pop();
7093
+ out.push({ kind: "DEDENT", value: "", line: nextTok.line, col: 1 });
7094
+ }
7095
+ }
7096
+ }
7097
+ while (indentStack.length > 1) {
7098
+ indentStack.pop();
7099
+ out.push({ kind: "DEDENT", value: "", line: 0, col: 0 });
7100
+ }
7101
+ out.push({ kind: "EOF", value: "", line: this._line, col: this._col });
7102
+ return out;
7103
+ }
7104
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
7105
+ _make(kind, value) {
7106
+ return { kind, value, line: this._line, col: this._col };
7107
+ }
7108
+ _peek(offset) {
7109
+ return this._src[this._pos + offset] ?? "";
7110
+ }
7111
+ _adv(n = 1) {
7112
+ this._pos += n;
7113
+ this._col += n;
7114
+ }
7115
+ _isDigit(ch) {
7116
+ return ch >= "0" && ch <= "9";
7117
+ }
7118
+ _isAlpha(ch) {
7119
+ return ch >= "a" && ch <= "z" || ch >= "A" && ch <= "Z" || ch === "_";
7120
+ }
7121
+ _isAlphaNum(ch) {
7122
+ return this._isAlpha(ch) || this._isDigit(ch);
7123
+ }
7124
+ _readNumber() {
7125
+ const start = this._pos;
7126
+ const startCol = this._col;
7127
+ while (this._pos < this._src.length) {
7128
+ const c = this._src[this._pos];
7129
+ if (this._isDigit(c) || c === ".") {
7130
+ this._pos++;
7131
+ this._col++;
7132
+ } else break;
7133
+ }
7134
+ return { kind: "NUMBER", value: this._src.slice(start, this._pos), line: this._line, col: startCol };
7135
+ }
7136
+ _readString(quote) {
7137
+ const startCol = this._col;
7138
+ this._pos++;
7139
+ this._col++;
7140
+ let result = "";
7141
+ while (this._pos < this._src.length && this._src[this._pos] !== quote) {
7142
+ if (this._src[this._pos] === "\\") {
7143
+ this._pos++;
7144
+ this._col++;
7145
+ const esc = this._src[this._pos] ?? "";
7146
+ result += esc === "n" ? "\n" : esc === "t" ? " " : esc;
7147
+ } else {
7148
+ result += this._src[this._pos];
7149
+ }
7150
+ this._pos++;
7151
+ this._col++;
7152
+ }
7153
+ this._pos++;
7154
+ this._col++;
7155
+ return { kind: "STRING", value: result, line: this._line, col: startCol };
7156
+ }
7157
+ _readIdent() {
7158
+ const start = this._pos;
7159
+ const startCol = this._col;
7160
+ while (this._pos < this._src.length && this._isAlphaNum(this._src[this._pos])) {
7161
+ this._pos++;
7162
+ this._col++;
7163
+ }
7164
+ const name = this._src.slice(start, this._pos);
7165
+ if (name === "true" || name === "false") {
7166
+ return { kind: "BOOL", value: name, line: this._line, col: startCol };
7167
+ }
7168
+ return { kind: "IDENT", value: name, line: this._line, col: startCol };
7169
+ }
7170
+ };
7171
+
7172
+ // src/pine/pine-parser.ts
7173
+ var PineParser = class {
7174
+ _tokens;
7175
+ _pos = 0;
7176
+ version;
7177
+ constructor(src) {
7178
+ const lexer = new PineLexer(src);
7179
+ this._tokens = lexer.tokenize();
7180
+ this.version = lexer.version;
7181
+ }
7182
+ parse() {
7183
+ const stmts = [];
7184
+ this._skipNL();
7185
+ while (!this._check("EOF")) {
7186
+ const s = this._stmt();
7187
+ if (s !== null) stmts.push(s);
7188
+ this._skipNL();
7189
+ }
7190
+ return { kind: "PineProgram", version: this.version, stmts };
7191
+ }
7192
+ // ─── Statements ─────────────────────────────────────────────────────────────
7193
+ _stmt() {
7194
+ const t = this._cur();
7195
+ const loc = { line: t.line, col: t.col };
7196
+ if (t.kind === "IDENT" && (t.value === "var" || t.value === "varip")) {
7197
+ return this._varDecl();
7198
+ }
7199
+ if (t.kind === "IDENT" && t.value === "if") {
7200
+ return this._ifStmt();
7201
+ }
7202
+ if (t.kind === "IDENT") {
7203
+ const p1 = this._peekKind(1);
7204
+ const p2 = this._peekKind(2);
7205
+ const isAssign = p1 === "ASSIGN" || p1 === "REASSIGN";
7206
+ const isTyped = p1 === "IDENT" && (p2 === "ASSIGN" || p2 === "REASSIGN");
7207
+ if (isAssign || isTyped) {
7208
+ return this._assignStmt();
7209
+ }
7210
+ }
7211
+ if (t.kind === "IDENT" && t.value === "indicator") {
7212
+ return this._indicatorDecl();
7213
+ }
7214
+ const expr = this._expr();
7215
+ this._consumeNLorEOF();
7216
+ return { kind: "PineExprStmt", expr, loc };
7217
+ }
7218
+ _varDecl() {
7219
+ const tok = this._advance();
7220
+ const modifier = tok.value;
7221
+ const loc = { line: tok.line, col: tok.col };
7222
+ let typeHint;
7223
+ if (this._check("IDENT") && this._peekKind(1) === "ASSIGN") {
7224
+ const maybeType = this._cur().value;
7225
+ if (["float", "int", "bool", "string", "color", "series", "simple", "const"].includes(maybeType)) {
7226
+ typeHint = maybeType;
7227
+ this._advance();
7228
+ }
7229
+ }
7230
+ const name = this._consume("IDENT", "Expected variable name after 'var'").value;
7231
+ this._consume("ASSIGN", "Expected '=' in var declaration");
7232
+ const value = this._expr();
7233
+ this._consumeNLorEOF();
7234
+ return typeHint !== void 0 ? { kind: "PineVarDecl", modifier, name, typeHint, value, loc } : { kind: "PineVarDecl", modifier, name, value, loc };
7235
+ }
7236
+ _assignStmt() {
7237
+ const nameTok = this._advance();
7238
+ const loc = { line: nameTok.line, col: nameTok.col };
7239
+ const name = nameTok.value;
7240
+ let op = "=";
7241
+ if (this._check("IDENT")) {
7242
+ this._advance();
7243
+ }
7244
+ if (this._check("REASSIGN")) {
7245
+ op = ":=";
7246
+ this._advance();
7247
+ } else {
7248
+ this._consume("ASSIGN", "Expected '=' or ':='");
7249
+ }
7250
+ const value = this._expr();
7251
+ this._consumeNLorEOF();
7252
+ return { kind: "PineAssign", name, op, value, loc };
7253
+ }
7254
+ _indicatorDecl() {
7255
+ const tok = this._advance();
7256
+ const loc = { line: tok.line, col: tok.col };
7257
+ this._consume("LPAREN", "Expected '(' after 'indicator'");
7258
+ const { args, namedArgs } = this._argList();
7259
+ this._consume("RPAREN", "Expected ')' after indicator args");
7260
+ this._consumeNLorEOF();
7261
+ return { kind: "PineIndicatorDecl", args, namedArgs, loc };
7262
+ }
7263
+ _ifStmt() {
7264
+ const tok = this._advance();
7265
+ const loc = { line: tok.line, col: tok.col };
7266
+ const condition = this._expr();
7267
+ this._consumeNLorEOF();
7268
+ const then = this._indentedBlock();
7269
+ const else_ = [];
7270
+ if (this._check("IDENT") && this._cur().value === "else") {
7271
+ this._advance();
7272
+ this._consumeNLorEOF();
7273
+ else_.push(...this._indentedBlock());
7274
+ }
7275
+ return { kind: "PineIf", condition, then, else_, loc };
7276
+ }
7277
+ _indentedBlock() {
7278
+ const stmts = [];
7279
+ if (!this._check("INDENT")) return stmts;
7280
+ this._advance();
7281
+ this._skipNL();
7282
+ while (!this._check("DEDENT") && !this._check("EOF")) {
7283
+ const s = this._stmt();
7284
+ if (s !== null) stmts.push(s);
7285
+ this._skipNL();
7286
+ }
7287
+ if (this._check("DEDENT")) this._advance();
7288
+ return stmts;
7289
+ }
7290
+ // ─── Expressions ────────────────────────────────────────────────────────────
7291
+ _expr() {
7292
+ return this._ternary();
7293
+ }
7294
+ _ternary() {
7295
+ let left = this._or();
7296
+ if (this._match("QMARK")) {
7297
+ const consequent = this._expr();
7298
+ this._consume("COLON", "Expected ':' in ternary");
7299
+ const alternate = this._expr();
7300
+ const node = {
7301
+ kind: "PineTernary",
7302
+ condition: left,
7303
+ consequent,
7304
+ alternate,
7305
+ loc: this._locOf(left)
7306
+ };
7307
+ return node;
7308
+ }
7309
+ return left;
7310
+ }
7311
+ _or() {
7312
+ let left = this._and();
7313
+ while (this._checkIdent("or")) {
7314
+ this._advance();
7315
+ const right = this._and();
7316
+ left = this._bin("or", left, right);
7317
+ }
7318
+ return left;
7319
+ }
7320
+ _and() {
7321
+ let left = this._not();
7322
+ while (this._checkIdent("and")) {
7323
+ this._advance();
7324
+ const right = this._not();
7325
+ left = this._bin("and", left, right);
7326
+ }
7327
+ return left;
7328
+ }
7329
+ _not() {
7330
+ if (this._checkIdent("not")) {
7331
+ const loc = this._locOfCur();
7332
+ this._advance();
7333
+ const operand = this._comparison();
7334
+ const node = { kind: "PineUnary", op: "not", operand, loc };
7335
+ return node;
7336
+ }
7337
+ return this._comparison();
7338
+ }
7339
+ _comparison() {
7340
+ let left = this._addition();
7341
+ while (true) {
7342
+ if (this._match("LTE")) {
7343
+ left = this._bin("<=", left, this._addition());
7344
+ continue;
7345
+ }
7346
+ if (this._match("GTE")) {
7347
+ left = this._bin(">=", left, this._addition());
7348
+ continue;
7349
+ }
7350
+ if (this._match("EQEQ")) {
7351
+ left = this._bin("==", left, this._addition());
7352
+ continue;
7353
+ }
7354
+ if (this._match("NEQ")) {
7355
+ left = this._bin("!=", left, this._addition());
7356
+ continue;
7357
+ }
7358
+ if (this._match("LT")) {
7359
+ left = this._bin("<", left, this._addition());
7360
+ continue;
7361
+ }
7362
+ if (this._match("GT")) {
7363
+ left = this._bin(">", left, this._addition());
7364
+ continue;
7365
+ }
7366
+ break;
7367
+ }
7368
+ return left;
7369
+ }
7370
+ _addition() {
7371
+ let left = this._multiply();
7372
+ while (true) {
7373
+ if (this._match("PLUS")) {
7374
+ left = this._bin("+", left, this._multiply());
7375
+ continue;
7376
+ }
7377
+ if (this._match("MINUS")) {
7378
+ left = this._bin("-", left, this._multiply());
7379
+ continue;
7380
+ }
7381
+ break;
7382
+ }
7383
+ return left;
7384
+ }
7385
+ _multiply() {
7386
+ let left = this._unary();
7387
+ while (true) {
7388
+ if (this._match("STAR")) {
7389
+ left = this._bin("*", left, this._unary());
7390
+ continue;
7391
+ }
7392
+ if (this._match("SLASH")) {
7393
+ left = this._bin("/", left, this._unary());
7394
+ continue;
7395
+ }
7396
+ if (this._match("PERCENT")) {
7397
+ left = this._bin("%", left, this._unary());
7398
+ continue;
7399
+ }
7400
+ break;
7401
+ }
7402
+ return left;
7403
+ }
7404
+ _unary() {
7405
+ if (this._match("MINUS")) {
7406
+ const operand = this._unary();
7407
+ const node = { kind: "PineUnary", op: "-", operand, loc: this._locOfCur() };
7408
+ return node;
7409
+ }
7410
+ return this._postfix();
7411
+ }
7412
+ _postfix() {
7413
+ let expr = this._primary();
7414
+ while (this._check("LBRACKET")) {
7415
+ const loc = this._locOfCur();
7416
+ this._advance();
7417
+ const index = this._expr();
7418
+ this._consume("RBRACKET", "Expected ']'");
7419
+ const node = { kind: "PineIndex", series: expr, index, loc };
7420
+ expr = node;
7421
+ }
7422
+ return expr;
7423
+ }
7424
+ _primary() {
7425
+ const t = this._cur();
7426
+ const loc = { line: t.line, col: t.col };
7427
+ if (t.kind === "NUMBER") {
7428
+ this._advance();
7429
+ const node = { kind: "PineNumberLit", value: parseFloat(t.value), loc };
7430
+ return node;
7431
+ }
7432
+ if (t.kind === "STRING") {
7433
+ this._advance();
7434
+ const node = { kind: "PineStringLit", value: t.value, loc };
7435
+ return node;
7436
+ }
7437
+ if (t.kind === "BOOL") {
7438
+ this._advance();
7439
+ const node = { kind: "PineBoolLit", value: t.value === "true", loc };
7440
+ return node;
7441
+ }
7442
+ if (t.kind === "IDENT" && t.value === "na") {
7443
+ this._advance();
7444
+ const node = { kind: "PineNa", loc };
7445
+ return node;
7446
+ }
7447
+ if (t.kind === "IDENT" && this._peekKind(1) === "DOT") {
7448
+ const ns = t.value;
7449
+ this._advance();
7450
+ this._advance();
7451
+ const member = this._consume("IDENT", "Expected member name after '.'").value;
7452
+ if (this._check("LPAREN")) {
7453
+ this._advance();
7454
+ const { args, namedArgs } = this._argList();
7455
+ this._consume("RPAREN", "Expected ')'");
7456
+ const node2 = { kind: "PineNsCall", namespace: ns, fn: member, args, namedArgs, loc };
7457
+ return node2;
7458
+ }
7459
+ const node = { kind: "PineMember", object: ns, prop: member, loc };
7460
+ return node;
7461
+ }
7462
+ if (t.kind === "IDENT" && this._peekKind(1) === "LPAREN") {
7463
+ const fn = t.value;
7464
+ this._advance();
7465
+ this._advance();
7466
+ const { args, namedArgs } = this._argList();
7467
+ this._consume("RPAREN", "Expected ')'");
7468
+ const node = { kind: "PineCall", fn, args, namedArgs, loc };
7469
+ return node;
7470
+ }
7471
+ if (t.kind === "IDENT") {
7472
+ this._advance();
7473
+ const node = { kind: "PineIdent", name: t.value, loc };
7474
+ return node;
7475
+ }
7476
+ if (t.kind === "LPAREN") {
7477
+ this._advance();
7478
+ const inner = this._expr();
7479
+ this._consume("RPAREN", "Expected ')'");
7480
+ return inner;
7481
+ }
7482
+ throw new SyntaxError(`[Pine] Unexpected token '${t.value}' (${t.kind}) at ${t.line}:${t.col}`);
7483
+ }
7484
+ // ─── Arg list helper ────────────────────────────────────────────────────────
7485
+ _argList() {
7486
+ const args = [];
7487
+ const namedArgs = /* @__PURE__ */ new Map();
7488
+ if (this._check("RPAREN")) return { args, namedArgs };
7489
+ this._parseOneArg(args, namedArgs);
7490
+ while (this._match("COMMA")) {
7491
+ if (this._check("RPAREN")) break;
7492
+ this._parseOneArg(args, namedArgs);
7493
+ }
7494
+ return { args, namedArgs };
7495
+ }
7496
+ _parseOneArg(args, namedArgs) {
7497
+ if (this._check("IDENT") && this._peekKind(1) === "ASSIGN" && this._peekKind(2) !== "ASSIGN") {
7498
+ const key = this._advance().value;
7499
+ this._advance();
7500
+ const val = this._expr();
7501
+ namedArgs.set(key, val);
7502
+ } else {
7503
+ args.push(this._expr());
7504
+ }
7505
+ }
7506
+ // ─── Token helpers ───────────────────────────────────────────────────────────
7507
+ _cur() {
7508
+ return this._tokens[this._pos];
7509
+ }
7510
+ _advance() {
7511
+ return this._tokens[this._pos++];
7512
+ }
7513
+ _check(kind) {
7514
+ return this._cur().kind === kind;
7515
+ }
7516
+ _checkIdent(name) {
7517
+ const t = this._cur();
7518
+ return t.kind === "IDENT" && t.value === name;
7519
+ }
7520
+ _peekKind(offset) {
7521
+ return this._tokens[this._pos + offset]?.kind ?? "EOF";
7522
+ }
7523
+ _match(kind) {
7524
+ if (this._check(kind)) {
7525
+ this._advance();
7526
+ return true;
7527
+ }
7528
+ return false;
7529
+ }
7530
+ _consume(kind, msg) {
7531
+ if (this._check(kind)) return this._advance();
7532
+ const t = this._cur();
7533
+ throw new SyntaxError(`[Pine] ${msg} \u2014 got '${t.value}' (${t.kind}) at ${t.line}:${t.col}`);
7534
+ }
7535
+ _skipNL() {
7536
+ while (this._check("NEWLINE")) this._advance();
7537
+ }
7538
+ _consumeNLorEOF() {
7539
+ if (this._check("NEWLINE") || this._check("EOF")) {
7540
+ if (this._check("NEWLINE")) this._advance();
7541
+ }
7542
+ }
7543
+ _locOfCur() {
7544
+ return { line: this._cur().line, col: this._cur().col };
7545
+ }
7546
+ _locOf(expr) {
7547
+ return expr.loc;
7548
+ }
7549
+ _bin(op, left, right) {
7550
+ return { kind: "PineBinary", op, left, right, loc: this._locOf(left) };
7551
+ }
7552
+ };
7553
+
7554
+ // src/pine/pine-transpiler.ts
7555
+ var TA_MAP = {
7556
+ sma: "sma",
7557
+ ema: "ema",
7558
+ rsi: "rsi",
7559
+ wma: "wma",
7560
+ rma: "rma",
7561
+ stdev: "stdev",
7562
+ highest: "highest",
7563
+ lowest: "lowest",
7564
+ change: "change",
7565
+ mom: "mom",
7566
+ atr: "atr",
7567
+ crossover: "crossover",
7568
+ crossunder: "crossunder",
7569
+ barssince: "barssince"
7570
+ };
7571
+ var MATH_MAP = {
7572
+ abs: "abs",
7573
+ max: "max",
7574
+ min: "min",
7575
+ round: "round",
7576
+ floor: "floor",
7577
+ ceil: "ceil",
7578
+ sqrt: "sqrt",
7579
+ log: "log",
7580
+ pow: "pow",
7581
+ sign: "sign",
7582
+ exp: "exp"
7583
+ };
7584
+ var COLOR_MAP = {
7585
+ red: '"#f23645"',
7586
+ green: '"#26a69a"',
7587
+ blue: '"#2196f3"',
7588
+ white: '"#ffffff"',
7589
+ black: '"#000000"',
7590
+ yellow: '"#ffeb3b"',
7591
+ orange: '"#ff9800"',
7592
+ purple: '"#9c27b0"',
7593
+ aqua: '"#00bcd4"',
7594
+ gray: '"#787b86"',
7595
+ silver: '"#b2b5be"',
7596
+ lime: '"#4caf50"',
7597
+ maroon: '"#880000"',
7598
+ navy: '"#000080"',
7599
+ olive: '"#808000"',
7600
+ teal: '"#008080"',
7601
+ fuchsia: '"#ff00ff"'
7602
+ };
7603
+ var UNSUPPORTED_FN = /* @__PURE__ */ new Set([
7604
+ "plotshape",
7605
+ "plotarrow",
7606
+ "plotbar",
7607
+ "plotcandle",
7608
+ "barcolor",
7609
+ "bgcolor",
7610
+ "alertcondition",
7611
+ "alert",
7612
+ "strategy",
7613
+ "label",
7614
+ "line",
7615
+ "box",
7616
+ "table",
7617
+ "request"
7618
+ ]);
7619
+ var PineTranspiler = class {
7620
+ _diag;
7621
+ _indent = 0;
7622
+ _out = [];
7623
+ constructor(diag) {
7624
+ this._diag = diag;
7625
+ }
7626
+ transpile(prog) {
7627
+ this._out = [];
7628
+ this._indent = 0;
7629
+ for (const stmt of prog.stmts) {
7630
+ this._stmt(stmt);
7631
+ }
7632
+ return this._out.join("");
7633
+ }
7634
+ // ─── Statements ─────────────────────────────────────────────────────────────
7635
+ _stmt(s) {
7636
+ switch (s.kind) {
7637
+ case "PineIndicatorDecl":
7638
+ return this._indicatorDecl(s);
7639
+ case "PineVarDecl":
7640
+ return this._varDecl(s);
7641
+ case "PineAssign":
7642
+ return this._assign(s);
7643
+ case "PineExprStmt":
7644
+ return this._exprStmt(s);
7645
+ case "PineIf":
7646
+ return this._ifStmt(s);
7647
+ }
7648
+ }
7649
+ _indicatorDecl(s) {
7650
+ const title = s.args[0];
7651
+ const titleStr = title ? this._expr(title) : '"Script"';
7652
+ this._line(`indicator(${titleStr})`);
7653
+ }
7654
+ _varDecl(s) {
7655
+ this._line(`${s.name} = ${this._expr(s.value)}`);
7656
+ }
7657
+ _assign(s) {
7658
+ this._line(`${s.name} = ${this._expr(s.value)}`);
7659
+ }
7660
+ _exprStmt(s) {
7661
+ const expr = s.expr;
7662
+ if ((expr.kind === "PineCall" || expr.kind === "PineNsCall") && UNSUPPORTED_FN.has(expr.kind === "PineCall" ? expr.fn : expr.fn)) {
7663
+ const name = expr.kind === "PineCall" ? expr.fn : `${expr.namespace}.${expr.fn}`;
7664
+ this._diag.add("warning", `'${name}()' is not supported and will be ignored`, s.loc, "PINE_UNSUPPORTED_FN");
7665
+ this._line(`// ${name}() not supported`);
7666
+ return;
7667
+ }
7668
+ this._line(this._expr(expr));
7669
+ }
7670
+ _ifStmt(s) {
7671
+ this._line(`if ${this._expr(s.condition)}`);
7672
+ this._indent++;
7673
+ for (const st of s.then) this._stmt(st);
7674
+ if (s.then.length === 0) this._line("0");
7675
+ this._indent--;
7676
+ if (s.else_.length > 0) {
7677
+ this._line("else");
7678
+ this._indent++;
7679
+ for (const st of s.else_) this._stmt(st);
7680
+ this._indent--;
7681
+ }
7682
+ }
7683
+ // ─── Expressions ────────────────────────────────────────────────────────────
7684
+ _expr(e) {
7685
+ switch (e.kind) {
7686
+ case "PineNumberLit":
7687
+ return this._numLit(e);
7688
+ case "PineStringLit":
7689
+ return `"${e.value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
7690
+ case "PineBoolLit":
7691
+ return e.value ? "true" : "false";
7692
+ case "PineNa":
7693
+ return "na";
7694
+ case "PineIdent":
7695
+ return e.name;
7696
+ case "PineNsCall":
7697
+ return this._nsCall(e);
7698
+ case "PineCall":
7699
+ return this._call(e);
7700
+ case "PineIndex":
7701
+ return `${this._expr(e.series)}[${this._expr(e.index)}]`;
7702
+ case "PineBinary":
7703
+ return this._binary(e);
7704
+ case "PineUnary":
7705
+ return this._unary(e);
7706
+ case "PineTernary":
7707
+ return `${this._expr(e.condition)} ? ${this._expr(e.consequent)} : ${this._expr(e.alternate)}`;
7708
+ case "PineMember":
7709
+ return this._member(e);
7710
+ // PineColorLit is resolved via PineMember for color.red etc
7711
+ default:
7712
+ return "0";
7713
+ }
7714
+ }
7715
+ _numLit(e) {
7716
+ return Number.isInteger(e.value) ? String(e.value) : String(e.value);
7717
+ }
7718
+ _nsCall(e) {
7719
+ const { namespace: ns, fn } = e;
7720
+ if (ns === "ta") {
7721
+ const mapped = TA_MAP[fn];
7722
+ if (mapped) {
7723
+ const args = this._positionalArgs(e.args);
7724
+ return `${mapped}(${args})`;
7725
+ }
7726
+ this._diag.add("warning", `'ta.${fn}()' is not supported`, e.loc, "PINE_UNSUPPORTED_FN");
7727
+ return `/* ta.${fn} not supported */ 0`;
7728
+ }
7729
+ if (ns === "math") {
7730
+ const mapped = MATH_MAP[fn];
7731
+ if (mapped) {
7732
+ const args = this._positionalArgs(e.args);
7733
+ return `${mapped}(${args})`;
7734
+ }
7735
+ this._diag.add("warning", `'math.${fn}()' is not supported`, e.loc, "PINE_UNSUPPORTED_FN");
7736
+ return `/* math.${fn} not supported */ 0`;
7737
+ }
7738
+ if (ns === "input") {
7739
+ return this._inputNsCall(e);
7740
+ }
7741
+ if (ns === "strategy" || ns === "label" || ns === "line" || ns === "box" || ns === "table" || ns === "request") {
7742
+ this._diag.add("warning", `'${ns}.${fn}()' is not supported`, e.loc, "PINE_UNSUPPORTED_FN");
7743
+ return `/* ${ns}.${fn} not supported */ 0`;
7744
+ }
7745
+ this._diag.add("warning", `'${ns}.${fn}()' is not supported`, e.loc, "PINE_UNSUPPORTED_FN");
7746
+ return `/* ${ns}.${fn} not supported */ 0`;
7747
+ }
7748
+ _inputNsCall(e) {
7749
+ if (e.fn === "source") {
7750
+ return e.args[0] ? this._expr(e.args[0]) : "close";
7751
+ }
7752
+ const defVal = e.args[0] ?? e.namedArgs.get("defval");
7753
+ return defVal ? `input(${this._expr(defVal)})` : "input(0)";
7754
+ }
7755
+ _call(e) {
7756
+ if (UNSUPPORTED_FN.has(e.fn)) {
7757
+ this._diag.add("warning", `'${e.fn}()' is not supported and will be ignored`, e.loc, "PINE_UNSUPPORTED_FN");
7758
+ return `/* ${e.fn} not supported */ 0`;
7759
+ }
7760
+ if (e.fn === "input") {
7761
+ const defVal = e.args[0] ?? e.namedArgs.get("defval");
7762
+ return defVal ? `input(${this._expr(defVal)})` : "input(0)";
7763
+ }
7764
+ if (e.fn === "plot") {
7765
+ return this._plotCall(e);
7766
+ }
7767
+ if (e.fn === "indicator") {
7768
+ const title = e.args[0];
7769
+ const titleStr = title ? this._expr(title) : '"Script"';
7770
+ return `indicator(${titleStr})`;
7771
+ }
7772
+ const args = this._positionalArgs(e.args);
7773
+ return `${e.fn}(${args})`;
7774
+ }
7775
+ _plotCall(e) {
7776
+ const series = e.args[0];
7777
+ if (!series) return "/* plot() missing series */";
7778
+ return `plot(${this._expr(series)})`;
7779
+ }
7780
+ _member(e) {
7781
+ if (e.object === "color") {
7782
+ const hex = COLOR_MAP[e.prop];
7783
+ if (hex) return hex;
7784
+ this._diag.add("info", `'color.${e.prop}' is not a known color`, e.loc, "PINE_UNKNOWN_COLOR");
7785
+ return '"#888888"';
7786
+ }
7787
+ if (e.object === "line" || e.object === "label" || e.object === "box") {
7788
+ return `"${e.prop}"`;
7789
+ }
7790
+ if (e.object === "timeframe" || e.object === "syminfo" || e.object === "session") {
7791
+ this._diag.add("info", `'${e.object}.${e.prop}' is not supported`, e.loc, "PINE_UNSUPPORTED_FN");
7792
+ return "0";
7793
+ }
7794
+ return `${e.object}_${e.prop}`;
7795
+ }
7796
+ _binary(e) {
7797
+ const l = this._expr(e.left);
7798
+ const r = this._expr(e.right);
7799
+ return `(${l} ${e.op} ${r})`;
7800
+ }
7801
+ _unary(e) {
7802
+ if (e.op === "not") return `not ${this._expr(e.operand)}`;
7803
+ return `${e.op}${this._expr(e.operand)}`;
7804
+ }
7805
+ _positionalArgs(args) {
7806
+ return args.map((a) => this._expr(a)).join(", ");
7807
+ }
7808
+ // ─── Output helpers ─────────────────────────────────────────────────────────
7809
+ _line(code) {
7810
+ const pad = " ".repeat(this._indent);
7811
+ this._out.push(`${pad}${code}
7812
+ `);
7813
+ }
7814
+ };
7815
+
7816
+ // src/pine/diagnostics.ts
7817
+ var DiagnosticBag = class {
7818
+ _diags = [];
7819
+ add(severity, message, loc, code) {
7820
+ this._diags.push({ severity, message, line: loc.line, col: loc.col, code });
7821
+ }
7822
+ get diagnostics() {
7823
+ return this._diags;
7824
+ }
7825
+ hasErrors() {
7826
+ return this._diags.some((d) => d.severity === "error");
7827
+ }
7828
+ };
7829
+
7830
+ // src/pine/PineCompiler.ts
7831
+ var PineCompiler = class _PineCompiler {
7832
+ /**
7833
+ * Compile Pine Script source to TScript.
7834
+ * Parse errors throw a `SyntaxError`; semantic problems populate diagnostics.
7835
+ */
7836
+ compile(pineSource) {
7837
+ const diag = new DiagnosticBag();
7838
+ const parser = new PineParser(pineSource);
7839
+ const program = parser.parse();
7840
+ const transpiler = new PineTranspiler(diag);
7841
+ const tscript = transpiler.transpile(program);
7842
+ return {
7843
+ tscript,
7844
+ diagnostics: diag.diagnostics,
7845
+ ok: !diag.hasErrors()
7846
+ };
7847
+ }
7848
+ /**
7849
+ * Convenience: compile + run against a bar series.
7850
+ * Returns the plot outputs and any diagnostics.
7851
+ */
7852
+ static run(pineSource, bars) {
7853
+ const compiler = new _PineCompiler();
7854
+ const result = compiler.compile(pineSource);
7855
+ if (!result.ok) {
7856
+ return { plots: [], diagnostics: result.diagnostics };
7857
+ }
7858
+ const indicator = new TScriptIndicator(result.tscript);
7859
+ const plots = indicator.run(bars);
7860
+ return { plots, diagnostics: result.diagnostics };
7861
+ }
7862
+ };
7863
+
6946
7864
  // src/api/drawing tools/pointers menu/dot.ts
6947
7865
  var DOT_RADIUS = 3;
6948
7866
  var DOT_COLOR = "var(--crosshair-overlay, rgba(255,255,255,0.75))";
@@ -10414,1146 +11332,2181 @@ function IndicatorsDialog({ onAdd, onClose }) {
10414
11332
  /* @__PURE__ */ jsx(
10415
11333
  "input",
10416
11334
  {
10417
- ref: inputRef,
10418
- className: "ind-search-input",
11335
+ ref: inputRef,
11336
+ className: "ind-search-input",
11337
+ type: "text",
11338
+ value: query,
11339
+ onChange: (e) => setQuery(e.target.value),
11340
+ placeholder: "Search indicators\u2026",
11341
+ autoComplete: "off",
11342
+ spellCheck: false
11343
+ }
11344
+ ),
11345
+ query && /* @__PURE__ */ jsx("button", { className: "ind-search-clear", onClick: () => setQuery(""), "aria-label": "Clear", children: /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 12 12", width: "10", height: "10", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", fill: "none", children: [
11346
+ /* @__PURE__ */ jsx("line", { x1: "2", y1: "2", x2: "10", y2: "10" }),
11347
+ /* @__PURE__ */ jsx("line", { x1: "10", y1: "2", x2: "2", y2: "10" })
11348
+ ] }) })
11349
+ ] }),
11350
+ /* @__PURE__ */ jsx("div", { className: "ind-tabs", children: CATEGORIES.map((cat) => /* @__PURE__ */ jsx(
11351
+ "button",
11352
+ {
11353
+ className: `ind-tab${cat === category ? " active" : ""}`,
11354
+ onClick: () => setCategory(cat),
11355
+ children: cat
11356
+ },
11357
+ cat
11358
+ )) }),
11359
+ /* @__PURE__ */ jsx("div", { className: "ind-list-wrap", children: visible.length === 0 ? /* @__PURE__ */ jsxs("div", { className: "ind-empty", children: [
11360
+ 'No indicators match "',
11361
+ query,
11362
+ '"'
11363
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
11364
+ /* @__PURE__ */ jsxs("div", { className: "ind-section-hdr", children: [
11365
+ /* @__PURE__ */ jsx("span", { children: "Built-in" }),
11366
+ /* @__PURE__ */ jsx("span", { className: "ind-section-count", children: visible.length })
11367
+ ] }),
11368
+ visible.map((entry) => {
11369
+ const isAdded = added === entry.type;
11370
+ return /* @__PURE__ */ jsxs(
11371
+ "button",
11372
+ {
11373
+ className: `ind-list-row${isAdded ? " added" : ""}`,
11374
+ onClick: () => handleAdd(entry),
11375
+ children: [
11376
+ /* @__PURE__ */ jsxs("div", { className: "ind-list-info", children: [
11377
+ /* @__PURE__ */ jsx("span", { className: "ind-list-name", children: entry.label }),
11378
+ /* @__PURE__ */ jsxs("span", { className: "ind-list-meta", children: [
11379
+ entry.author,
11380
+ " \xB7 ",
11381
+ entry.category
11382
+ ] })
11383
+ ] }),
11384
+ /* @__PURE__ */ jsx("span", { className: `ind-list-badge${entry.defaultConfig.overlay ? " overlay" : " pane"}`, children: entry.defaultConfig.overlay ? "Overlay" : "Pane" }),
11385
+ /* @__PURE__ */ jsx("span", { className: "ind-list-add", children: isAdded ? /* @__PURE__ */ jsxs(Fragment, { children: [
11386
+ /* @__PURE__ */ jsx("svg", { viewBox: "0 0 12 10", width: "11", height: "10", stroke: "currentColor", strokeWidth: "2.2", strokeLinecap: "round", strokeLinejoin: "round", fill: "none", children: /* @__PURE__ */ jsx("polyline", { points: "1,5 4,8 11,1" }) }),
11387
+ "Added"
11388
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
11389
+ /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 12 12", width: "11", height: "11", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", fill: "none", children: [
11390
+ /* @__PURE__ */ jsx("line", { x1: "6", y1: "1", x2: "6", y2: "11" }),
11391
+ /* @__PURE__ */ jsx("line", { x1: "1", y1: "6", x2: "11", y2: "6" })
11392
+ ] }),
11393
+ "Add to Chart"
11394
+ ] }) })
11395
+ ]
11396
+ },
11397
+ entry.type
11398
+ );
11399
+ })
11400
+ ] }) })
11401
+ ] }) });
11402
+ }
11403
+ var DEFAULT_FAVORITES = ["1m", "5m", "15m", "30m", "1h", "4h", "1d", "1w"];
11404
+ var TF_LABELS = {
11405
+ "1m": "1 minute",
11406
+ "2m": "2 minutes",
11407
+ "3m": "3 minutes",
11408
+ "4m": "4 minutes",
11409
+ "5m": "5 minutes",
11410
+ "10m": "10 minutes",
11411
+ "15m": "15 minutes",
11412
+ "20m": "20 minutes",
11413
+ "30m": "30 minutes",
11414
+ "45m": "45 minutes",
11415
+ "1h": "1 hour",
11416
+ "2h": "2 hours",
11417
+ "3h": "3 hours",
11418
+ "4h": "4 hours",
11419
+ "6h": "6 hours",
11420
+ "8h": "8 hours",
11421
+ "12h": "12 hours",
11422
+ "1d": "1 day",
11423
+ "2d": "2 days",
11424
+ "3d": "3 days",
11425
+ "1w": "1 week",
11426
+ "2w": "2 weeks",
11427
+ "1M": "1 month",
11428
+ "3M": "3 months",
11429
+ "6M": "6 months",
11430
+ "12M": "12 months"
11431
+ };
11432
+ var TF_GROUPS_ALL = [
11433
+ { label: "Minutes", items: ["1m", "2m", "3m", "4m", "5m", "10m", "15m", "20m", "30m", "45m"] },
11434
+ { label: "Hours", items: ["1h", "2h", "3h", "4h", "6h", "8h", "12h"] },
11435
+ { label: "Days", items: ["1d", "2d", "3d", "1w", "2w", "1M", "3M", "6M", "12M"] }
11436
+ ];
11437
+ function TimeframeDropdown({
11438
+ timeframe,
11439
+ customTimeframes,
11440
+ favorites,
11441
+ onSelect,
11442
+ onToggleFavorite,
11443
+ onAddCustom,
11444
+ onClose
11445
+ }) {
11446
+ const [collapsed, setCollapsed] = useState({});
11447
+ const [addingCustom, setAddingCustom] = useState(false);
11448
+ const [input, setInput] = useState("");
11449
+ const ref = useRef(null);
11450
+ useEffect(() => {
11451
+ const handler = (e) => {
11452
+ if (ref.current && !ref.current.contains(e.target)) onClose();
11453
+ };
11454
+ document.addEventListener("mousedown", handler);
11455
+ return () => document.removeEventListener("mousedown", handler);
11456
+ }, [onClose]);
11457
+ const commit = () => {
11458
+ const val = input.trim();
11459
+ if (!val) return;
11460
+ onAddCustom(val);
11461
+ onSelect(val);
11462
+ setInput("");
11463
+ onClose();
11464
+ };
11465
+ const toggleGroup = (label) => setCollapsed((prev) => ({ ...prev, [label]: !prev[label] }));
11466
+ return /* @__PURE__ */ jsxs("div", { ref, className: "tf-dropdown", children: [
11467
+ /* @__PURE__ */ jsx(
11468
+ "button",
11469
+ {
11470
+ className: "tf-dropdown-add-custom",
11471
+ onClick: () => setAddingCustom((a) => !a),
11472
+ children: "+ Add custom interval..."
11473
+ }
11474
+ ),
11475
+ addingCustom && /* @__PURE__ */ jsxs("div", { className: "tf-dropdown-add-row", children: [
11476
+ /* @__PURE__ */ jsx(
11477
+ "input",
11478
+ {
11479
+ className: "tf-dropdown-input",
10419
11480
  type: "text",
10420
- value: query,
10421
- onChange: (e) => setQuery(e.target.value),
10422
- placeholder: "Search indicators\u2026",
10423
- autoComplete: "off",
10424
- spellCheck: false
11481
+ value: input,
11482
+ placeholder: "e.g. 2h, 45m",
11483
+ onChange: (e) => setInput(e.target.value),
11484
+ onKeyDown: (e) => {
11485
+ if (e.key === "Enter") commit();
11486
+ },
11487
+ autoFocus: true
10425
11488
  }
10426
11489
  ),
10427
- query && /* @__PURE__ */ jsx("button", { className: "ind-search-clear", onClick: () => setQuery(""), "aria-label": "Clear", children: /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 12 12", width: "10", height: "10", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", fill: "none", children: [
10428
- /* @__PURE__ */ jsx("line", { x1: "2", y1: "2", x2: "10", y2: "10" }),
10429
- /* @__PURE__ */ jsx("line", { x1: "10", y1: "2", x2: "2", y2: "10" })
10430
- ] }) })
11490
+ /* @__PURE__ */ jsx(
11491
+ "button",
11492
+ {
11493
+ className: "tf-dropdown-add-btn",
11494
+ disabled: !input.trim(),
11495
+ onClick: commit,
11496
+ children: "Add"
11497
+ }
11498
+ )
10431
11499
  ] }),
10432
- /* @__PURE__ */ jsx("div", { className: "ind-tabs", children: CATEGORIES.map((cat) => /* @__PURE__ */ jsx(
10433
- "button",
10434
- {
10435
- className: `ind-tab${cat === category ? " active" : ""}`,
10436
- onClick: () => setCategory(cat),
10437
- children: cat
11500
+ TF_GROUPS_ALL.map((group) => /* @__PURE__ */ jsxs("div", { children: [
11501
+ /* @__PURE__ */ jsxs(
11502
+ "button",
11503
+ {
11504
+ className: "tf-dropdown-group-hdr",
11505
+ onClick: () => toggleGroup(group.label),
11506
+ children: [
11507
+ /* @__PURE__ */ jsx("span", { children: group.label }),
11508
+ /* @__PURE__ */ jsx("span", { className: `tf-dropdown-chevron${collapsed[group.label] ? " collapsed" : ""}`, children: "\u2227" })
11509
+ ]
11510
+ }
11511
+ ),
11512
+ !collapsed[group.label] && group.items.map((tf) => /* @__PURE__ */ jsxs(
11513
+ "button",
11514
+ {
11515
+ className: "tf-dropdown-row",
11516
+ onClick: () => {
11517
+ onSelect(tf);
11518
+ onClose();
11519
+ },
11520
+ children: [
11521
+ /* @__PURE__ */ jsx("span", { children: TF_LABELS[tf] ?? tf }),
11522
+ /* @__PURE__ */ jsx(
11523
+ "button",
11524
+ {
11525
+ className: "tf-star-btn",
11526
+ title: favorites.includes(tf) ? "Remove from toolbar" : "Add to toolbar",
11527
+ onClick: (e) => {
11528
+ e.stopPropagation();
11529
+ onToggleFavorite(tf);
11530
+ },
11531
+ children: /* @__PURE__ */ jsx("span", { className: `tf-star${favorites.includes(tf) ? " favorited" : ""}`, children: "\u2605" })
11532
+ }
11533
+ )
11534
+ ]
11535
+ },
11536
+ tf
11537
+ ))
11538
+ ] }, group.label)),
11539
+ customTimeframes.length > 0 && /* @__PURE__ */ jsxs("div", { children: [
11540
+ /* @__PURE__ */ jsxs(
11541
+ "button",
11542
+ {
11543
+ className: "tf-dropdown-group-hdr",
11544
+ onClick: () => toggleGroup("Custom"),
11545
+ children: [
11546
+ /* @__PURE__ */ jsx("span", { children: "Custom" }),
11547
+ /* @__PURE__ */ jsx("span", { className: `tf-dropdown-chevron${collapsed["Custom"] ? " collapsed" : ""}`, children: "\u2227" })
11548
+ ]
11549
+ }
11550
+ ),
11551
+ !collapsed["Custom"] && customTimeframes.map((tf) => /* @__PURE__ */ jsx(
11552
+ "button",
11553
+ {
11554
+ className: "tf-dropdown-row",
11555
+ onClick: () => {
11556
+ onSelect(tf);
11557
+ onClose();
11558
+ },
11559
+ children: /* @__PURE__ */ jsx("span", { children: tf })
11560
+ },
11561
+ tf
11562
+ ))
11563
+ ] })
11564
+ ] });
11565
+ }
11566
+ function TopToolbar({
11567
+ symbol,
11568
+ timeframe,
11569
+ theme,
11570
+ customTimeframes,
11571
+ favorites,
11572
+ onSymbolChange,
11573
+ onTimeframeChange,
11574
+ onAddCustomTimeframe,
11575
+ onFavoritesChange,
11576
+ onAddIndicator,
11577
+ onToggleTheme,
11578
+ onCopyScreenshot,
11579
+ onDownloadScreenshot,
11580
+ onFullscreen,
11581
+ isFullscreen,
11582
+ currentLayoutName,
11583
+ currentLayoutId,
11584
+ autoSave,
11585
+ onSaveLayout,
11586
+ onLoadLayout,
11587
+ onRenameLayout,
11588
+ onCopyLayout,
11589
+ onToggleAutoSave,
11590
+ onDeleteLayout,
11591
+ onOpenLayoutInNewTab,
11592
+ onFetchLayouts,
11593
+ symbolResolver,
11594
+ showTradeButton,
11595
+ tradeDrawerOpen,
11596
+ onToggleTradeDrawer
11597
+ }) {
11598
+ const [tfOpen, setTfOpen] = useState(false);
11599
+ const [symOpen, setSymOpen] = useState(false);
11600
+ const [indOpen, setIndOpen] = useState(false);
11601
+ const [ssOpen, setSsOpen] = useState(false);
11602
+ const ssRef = useRef(null);
11603
+ const tfRef = useRef(null);
11604
+ useEffect(() => {
11605
+ if (!ssOpen) return;
11606
+ const handler = (e) => {
11607
+ if (ssRef.current && !ssRef.current.contains(e.target)) setSsOpen(false);
11608
+ };
11609
+ document.addEventListener("mousedown", handler);
11610
+ return () => document.removeEventListener("mousedown", handler);
11611
+ }, [ssOpen]);
11612
+ return /* @__PURE__ */ jsxs(
11613
+ "div",
11614
+ {
11615
+ style: {
11616
+ display: "flex",
11617
+ alignItems: "center",
11618
+ gap: 8,
11619
+ padding: "6px 12px",
11620
+ background: "var(--toolbar-bg)",
11621
+ borderRadius: 8,
11622
+ flexShrink: 0
10438
11623
  },
10439
- cat
10440
- )) }),
10441
- /* @__PURE__ */ jsx("div", { className: "ind-list-wrap", children: visible.length === 0 ? /* @__PURE__ */ jsxs("div", { className: "ind-empty", children: [
10442
- 'No indicators match "',
10443
- query,
10444
- '"'
10445
- ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
10446
- /* @__PURE__ */ jsxs("div", { className: "ind-section-hdr", children: [
10447
- /* @__PURE__ */ jsx("span", { children: "Built-in" }),
10448
- /* @__PURE__ */ jsx("span", { className: "ind-section-count", children: visible.length })
10449
- ] }),
10450
- visible.map((entry) => {
10451
- const isAdded = added === entry.type;
10452
- return /* @__PURE__ */ jsxs(
11624
+ children: [
11625
+ /* @__PURE__ */ jsx("span", { style: { fontWeight: 700, letterSpacing: "-0.5px", color: "var(--accent)" }, children: "ForgeCharts" }),
11626
+ /* @__PURE__ */ jsx("div", { className: "toolbar-sep" }),
11627
+ /* @__PURE__ */ jsxs(
10453
11628
  "button",
10454
11629
  {
10455
- className: `ind-list-row${isAdded ? " added" : ""}`,
10456
- onClick: () => handleAdd(entry),
11630
+ className: "sym-trigger",
11631
+ onClick: () => setSymOpen(true),
11632
+ title: "Search symbols",
11633
+ children: [
11634
+ /* @__PURE__ */ jsxs(
11635
+ "svg",
11636
+ {
11637
+ viewBox: "0 0 14 14",
11638
+ width: "12",
11639
+ height: "12",
11640
+ stroke: "currentColor",
11641
+ fill: "none",
11642
+ strokeWidth: "1.8",
11643
+ strokeLinecap: "round",
11644
+ children: [
11645
+ /* @__PURE__ */ jsx("circle", { cx: "5.5", cy: "5.5", r: "4" }),
11646
+ /* @__PURE__ */ jsx("line", { x1: "9", y1: "9", x2: "13", y2: "13" })
11647
+ ]
11648
+ }
11649
+ ),
11650
+ /* @__PURE__ */ jsx("span", { children: symbol }),
11651
+ /* @__PURE__ */ jsx("svg", { viewBox: "0 0 10 6", width: "8", height: "6", fill: "currentColor", children: /* @__PURE__ */ jsx("path", { d: "M0 0l5 6 5-6z" }) })
11652
+ ]
11653
+ }
11654
+ ),
11655
+ symOpen && /* @__PURE__ */ jsx(
11656
+ SymbolSearchDialog,
11657
+ {
11658
+ current: symbol,
11659
+ onSelect: onSymbolChange,
11660
+ onClose: () => setSymOpen(false),
11661
+ symbolResolver
11662
+ }
11663
+ ),
11664
+ /* @__PURE__ */ jsx("div", { className: "toolbar-sep" }),
11665
+ /* @__PURE__ */ jsxs("div", { ref: tfRef, style: { display: "flex", gap: 2, position: "relative" }, children: [
11666
+ favorites.map((tf) => /* @__PURE__ */ jsx(
11667
+ "button",
11668
+ {
11669
+ className: tf === timeframe ? "active" : void 0,
11670
+ onClick: () => onTimeframeChange(tf),
11671
+ children: tf
11672
+ },
11673
+ tf
11674
+ )),
11675
+ !favorites.includes(timeframe) && timeframe && /* @__PURE__ */ jsx("button", { className: "active", style: { fontStyle: "italic" }, children: timeframe }),
11676
+ /* @__PURE__ */ jsx(
11677
+ "button",
11678
+ {
11679
+ className: tfOpen ? "active" : void 0,
11680
+ onClick: () => setTfOpen((o) => !o),
11681
+ title: "More timeframes",
11682
+ style: { fontSize: 11, padding: "4px 7px" },
11683
+ children: "\u25BE"
11684
+ }
11685
+ ),
11686
+ tfOpen && /* @__PURE__ */ jsx(
11687
+ TimeframeDropdown,
11688
+ {
11689
+ timeframe,
11690
+ customTimeframes,
11691
+ favorites,
11692
+ onSelect: onTimeframeChange,
11693
+ onToggleFavorite: (tf) => {
11694
+ const next = favorites.includes(tf) ? favorites.filter((f) => f !== tf) : [...favorites, tf];
11695
+ onFavoritesChange(next);
11696
+ },
11697
+ onAddCustom: onAddCustomTimeframe,
11698
+ onClose: () => setTfOpen(false)
11699
+ }
11700
+ )
11701
+ ] }),
11702
+ /* @__PURE__ */ jsx("div", { className: "toolbar-sep" }),
11703
+ /* @__PURE__ */ jsxs(
11704
+ "button",
11705
+ {
11706
+ className: `ind-trigger${indOpen ? " active" : ""}`,
11707
+ onClick: () => setIndOpen((o) => !o),
11708
+ title: "Indicators",
10457
11709
  children: [
10458
- /* @__PURE__ */ jsxs("div", { className: "ind-list-info", children: [
10459
- /* @__PURE__ */ jsx("span", { className: "ind-list-name", children: entry.label }),
10460
- /* @__PURE__ */ jsxs("span", { className: "ind-list-meta", children: [
10461
- entry.author,
10462
- " \xB7 ",
10463
- entry.category
10464
- ] })
11710
+ /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 16 16", width: "13", height: "13", stroke: "currentColor", fill: "none", strokeWidth: "1.6", strokeLinecap: "round", children: [
11711
+ /* @__PURE__ */ jsx("polyline", { points: "2,12 6,7 9,10 14,4" }),
11712
+ /* @__PURE__ */ jsx("line", { x1: "11", y1: "2", x2: "14", y2: "2" }),
11713
+ /* @__PURE__ */ jsx("line", { x1: "14", y1: "2", x2: "14", y2: "5" })
10465
11714
  ] }),
10466
- /* @__PURE__ */ jsx("span", { className: `ind-list-badge${entry.defaultConfig.overlay ? " overlay" : " pane"}`, children: entry.defaultConfig.overlay ? "Overlay" : "Pane" }),
10467
- /* @__PURE__ */ jsx("span", { className: "ind-list-add", children: isAdded ? /* @__PURE__ */ jsxs(Fragment, { children: [
10468
- /* @__PURE__ */ jsx("svg", { viewBox: "0 0 12 10", width: "11", height: "10", stroke: "currentColor", strokeWidth: "2.2", strokeLinecap: "round", strokeLinejoin: "round", fill: "none", children: /* @__PURE__ */ jsx("polyline", { points: "1,5 4,8 11,1" }) }),
10469
- "Added"
10470
- ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
10471
- /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 12 12", width: "11", height: "11", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", fill: "none", children: [
10472
- /* @__PURE__ */ jsx("line", { x1: "6", y1: "1", x2: "6", y2: "11" }),
10473
- /* @__PURE__ */ jsx("line", { x1: "1", y1: "6", x2: "11", y2: "6" })
10474
- ] }),
10475
- "Add to Chart"
10476
- ] }) })
11715
+ "Indicators"
10477
11716
  ]
10478
- },
10479
- entry.type
10480
- );
10481
- })
10482
- ] }) })
10483
- ] }) });
10484
- }
10485
- var DEFAULT_FAVORITES = ["1m", "5m", "15m", "30m", "1h", "4h", "1d", "1w"];
10486
- var TF_LABELS = {
10487
- "1m": "1 minute",
10488
- "2m": "2 minutes",
10489
- "3m": "3 minutes",
10490
- "4m": "4 minutes",
10491
- "5m": "5 minutes",
10492
- "10m": "10 minutes",
10493
- "15m": "15 minutes",
10494
- "20m": "20 minutes",
10495
- "30m": "30 minutes",
10496
- "45m": "45 minutes",
10497
- "1h": "1 hour",
10498
- "2h": "2 hours",
10499
- "3h": "3 hours",
10500
- "4h": "4 hours",
10501
- "6h": "6 hours",
10502
- "8h": "8 hours",
10503
- "12h": "12 hours",
10504
- "1d": "1 day",
10505
- "2d": "2 days",
10506
- "3d": "3 days",
10507
- "1w": "1 week",
10508
- "2w": "2 weeks",
10509
- "1M": "1 month",
10510
- "3M": "3 months",
10511
- "6M": "6 months",
10512
- "12M": "12 months"
10513
- };
10514
- var TF_GROUPS_ALL = [
10515
- { label: "Minutes", items: ["1m", "2m", "3m", "4m", "5m", "10m", "15m", "20m", "30m", "45m"] },
10516
- { label: "Hours", items: ["1h", "2h", "3h", "4h", "6h", "8h", "12h"] },
10517
- { label: "Days", items: ["1d", "2d", "3d", "1w", "2w", "1M", "3M", "6M", "12M"] }
10518
- ];
10519
- function TimeframeDropdown({
10520
- timeframe,
10521
- customTimeframes,
10522
- favorites,
10523
- onSelect,
10524
- onToggleFavorite,
10525
- onAddCustom,
10526
- onClose
10527
- }) {
10528
- const [collapsed, setCollapsed] = useState({});
10529
- const [addingCustom, setAddingCustom] = useState(false);
10530
- const [input, setInput] = useState("");
10531
- const ref = useRef(null);
10532
- useEffect(() => {
10533
- const handler = (e) => {
10534
- if (ref.current && !ref.current.contains(e.target)) onClose();
10535
- };
10536
- document.addEventListener("mousedown", handler);
10537
- return () => document.removeEventListener("mousedown", handler);
10538
- }, [onClose]);
10539
- const commit = () => {
10540
- const val = input.trim();
10541
- if (!val) return;
10542
- onAddCustom(val);
10543
- onSelect(val);
10544
- setInput("");
10545
- onClose();
10546
- };
10547
- const toggleGroup = (label) => setCollapsed((prev) => ({ ...prev, [label]: !prev[label] }));
10548
- return /* @__PURE__ */ jsxs("div", { ref, className: "tf-dropdown", children: [
10549
- /* @__PURE__ */ jsx(
10550
- "button",
10551
- {
10552
- className: "tf-dropdown-add-custom",
10553
- onClick: () => setAddingCustom((a) => !a),
10554
- children: "+ Add custom interval..."
10555
- }
10556
- ),
10557
- addingCustom && /* @__PURE__ */ jsxs("div", { className: "tf-dropdown-add-row", children: [
10558
- /* @__PURE__ */ jsx(
10559
- "input",
10560
- {
10561
- className: "tf-dropdown-input",
10562
- type: "text",
10563
- value: input,
10564
- placeholder: "e.g. 2h, 45m",
10565
- onChange: (e) => setInput(e.target.value),
10566
- onKeyDown: (e) => {
10567
- if (e.key === "Enter") commit();
10568
- },
10569
- autoFocus: true
10570
- }
10571
- ),
10572
- /* @__PURE__ */ jsx(
10573
- "button",
10574
- {
10575
- className: "tf-dropdown-add-btn",
10576
- disabled: !input.trim(),
10577
- onClick: commit,
10578
- children: "Add"
10579
- }
10580
- )
10581
- ] }),
10582
- TF_GROUPS_ALL.map((group) => /* @__PURE__ */ jsxs("div", { children: [
10583
- /* @__PURE__ */ jsxs(
10584
- "button",
10585
- {
10586
- className: "tf-dropdown-group-hdr",
10587
- onClick: () => toggleGroup(group.label),
10588
- children: [
10589
- /* @__PURE__ */ jsx("span", { children: group.label }),
10590
- /* @__PURE__ */ jsx("span", { className: `tf-dropdown-chevron${collapsed[group.label] ? " collapsed" : ""}`, children: "\u2227" })
10591
- ]
10592
- }
10593
- ),
10594
- !collapsed[group.label] && group.items.map((tf) => /* @__PURE__ */ jsxs(
10595
- "button",
10596
- {
10597
- className: "tf-dropdown-row",
10598
- onClick: () => {
10599
- onSelect(tf);
10600
- onClose();
10601
- },
10602
- children: [
10603
- /* @__PURE__ */ jsx("span", { children: TF_LABELS[tf] ?? tf }),
11717
+ }
11718
+ ),
11719
+ indOpen && /* @__PURE__ */ jsx(
11720
+ IndicatorsDialog,
11721
+ {
11722
+ onAdd: (config) => {
11723
+ onAddIndicator(config);
11724
+ },
11725
+ onClose: () => setIndOpen(false)
11726
+ }
11727
+ ),
11728
+ /* @__PURE__ */ jsxs("div", { style: { marginLeft: "auto", display: "flex", alignItems: "center", gap: 4 }, children: [
11729
+ showTradeButton && /* @__PURE__ */ jsxs(Fragment, { children: [
11730
+ /* @__PURE__ */ jsxs(
11731
+ "button",
11732
+ {
11733
+ className: `top-trade-btn${tradeDrawerOpen ? " active" : ""}`,
11734
+ onClick: onToggleTradeDrawer,
11735
+ title: "Trade \u2014 Connect your broker",
11736
+ style: { display: "inline-flex", alignItems: "center", gap: 5, padding: "4px 8px", fontSize: 12 },
11737
+ children: [
11738
+ /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 16 16", width: "11", height: "11", fill: "none", stroke: "currentColor", strokeWidth: "1.6", children: [
11739
+ /* @__PURE__ */ jsx("rect", { x: "1.5", y: "3", width: "13", height: "10", rx: "1.5" }),
11740
+ /* @__PURE__ */ jsx("line", { x1: "5", y1: "8", x2: "11", y2: "8" }),
11741
+ /* @__PURE__ */ jsx("line", { x1: "8", y1: "5", x2: "8", y2: "11" })
11742
+ ] }),
11743
+ "Trade"
11744
+ ]
11745
+ }
11746
+ ),
11747
+ /* @__PURE__ */ jsx("div", { className: "toolbar-sep" })
11748
+ ] }),
11749
+ /* @__PURE__ */ jsx(
11750
+ LayoutMenu,
11751
+ {
11752
+ currentName: currentLayoutName,
11753
+ currentLayoutId,
11754
+ autoSave,
11755
+ onFetchLayouts,
11756
+ onSave: onSaveLayout,
11757
+ onLoad: onLoadLayout,
11758
+ onRename: onRenameLayout,
11759
+ onCopy: onCopyLayout,
11760
+ onToggleAutoSave,
11761
+ onDelete: onDeleteLayout,
11762
+ onOpenInNewTab: onOpenLayoutInNewTab
11763
+ }
11764
+ ),
11765
+ /* @__PURE__ */ jsx(
11766
+ "button",
11767
+ {
11768
+ onClick: onToggleTheme,
11769
+ title: `Switch to ${theme === "dark" ? "light" : "dark"} mode`,
11770
+ style: { fontSize: 14, padding: "4px 8px" },
11771
+ children: theme === "dark" ? "\u2600" : "\u263E"
11772
+ }
11773
+ ),
11774
+ /* @__PURE__ */ jsxs("div", { ref: ssRef, style: { position: "relative" }, children: [
10604
11775
  /* @__PURE__ */ jsx(
10605
11776
  "button",
10606
11777
  {
10607
- className: "tf-star-btn",
10608
- title: favorites.includes(tf) ? "Remove from toolbar" : "Add to toolbar",
10609
- onClick: (e) => {
10610
- e.stopPropagation();
10611
- onToggleFavorite(tf);
10612
- },
10613
- children: /* @__PURE__ */ jsx("span", { className: `tf-star${favorites.includes(tf) ? " favorited" : ""}`, children: "\u2605" })
11778
+ className: ssOpen ? "active" : void 0,
11779
+ onClick: () => setSsOpen((o) => !o),
11780
+ title: "Screenshot",
11781
+ style: { display: "inline-flex", alignItems: "center", padding: "4px 7px" },
11782
+ children: /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 16 16", width: "14", height: "14", stroke: "currentColor", fill: "none", strokeWidth: "1.5", children: [
11783
+ /* @__PURE__ */ jsx("rect", { x: "2", y: "4", width: "12", height: "9", rx: "1" }),
11784
+ /* @__PURE__ */ jsx("circle", { cx: "8", cy: "8.5", r: "2.5" }),
11785
+ /* @__PURE__ */ jsx("path", { d: "M6 4l1-2h2l1 2" })
11786
+ ] })
10614
11787
  }
10615
- )
10616
- ]
10617
- },
10618
- tf
10619
- ))
10620
- ] }, group.label)),
10621
- customTimeframes.length > 0 && /* @__PURE__ */ jsxs("div", { children: [
10622
- /* @__PURE__ */ jsxs(
11788
+ ),
11789
+ ssOpen && /* @__PURE__ */ jsxs("div", { className: "ss-dropdown", children: [
11790
+ /* @__PURE__ */ jsxs("button", { className: "ss-dropdown-item", onClick: () => {
11791
+ setSsOpen(false);
11792
+ onCopyScreenshot();
11793
+ }, children: [
11794
+ /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 16 16", width: "13", height: "13", stroke: "currentColor", fill: "none", strokeWidth: "1.5", children: [
11795
+ /* @__PURE__ */ jsx("rect", { x: "5", y: "5", width: "9", height: "9", rx: "1" }),
11796
+ /* @__PURE__ */ jsx("path", { d: "M3 11H2a1 1 0 01-1-1V2a1 1 0 011-1h8a1 1 0 011 1v1" })
11797
+ ] }),
11798
+ "Copy to Clipboard"
11799
+ ] }),
11800
+ /* @__PURE__ */ jsxs("button", { className: "ss-dropdown-item", onClick: () => {
11801
+ setSsOpen(false);
11802
+ onDownloadScreenshot();
11803
+ }, children: [
11804
+ /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 16 16", width: "13", height: "13", stroke: "currentColor", fill: "none", strokeWidth: "1.5", strokeLinecap: "round", children: [
11805
+ /* @__PURE__ */ jsx("line", { x1: "8", y1: "2", x2: "8", y2: "11" }),
11806
+ /* @__PURE__ */ jsx("polyline", { points: "4,7 8,11 12,7" }),
11807
+ /* @__PURE__ */ jsx("line", { x1: "2", y1: "14", x2: "14", y2: "14" })
11808
+ ] }),
11809
+ "Download PNG"
11810
+ ] })
11811
+ ] })
11812
+ ] }),
11813
+ /* @__PURE__ */ jsx(
11814
+ "button",
11815
+ {
11816
+ onClick: onFullscreen,
11817
+ title: isFullscreen ? "Exit Fullscreen" : "Fullscreen",
11818
+ style: { display: "inline-flex", alignItems: "center", padding: "4px 7px" },
11819
+ children: isFullscreen ? /* @__PURE__ */ jsx("svg", { viewBox: "0 0 16 16", width: "14", height: "14", stroke: "currentColor", fill: "none", strokeWidth: "1.5", children: /* @__PURE__ */ jsx("path", { d: "M6 2v4H2M10 2v4h4M10 14v-4h4M6 14v-4H2" }) }) : /* @__PURE__ */ jsx("svg", { viewBox: "0 0 16 16", width: "14", height: "14", stroke: "currentColor", fill: "none", strokeWidth: "1.5", children: /* @__PURE__ */ jsx("path", { d: "M2 6V2h4M10 2h4v4M14 10v4h-4M6 14H2v-4" }) })
11820
+ }
11821
+ )
11822
+ ] })
11823
+ ]
11824
+ }
11825
+ );
11826
+ }
11827
+ var WatchListIcon = () => /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 16 16", width: "16", height: "16", stroke: "currentColor", fill: "none", strokeWidth: "1.5", children: [
11828
+ /* @__PURE__ */ jsx("rect", { x: "2", y: "3", width: "12", height: "1.5", rx: "0.5", fill: "currentColor", stroke: "none" }),
11829
+ /* @__PURE__ */ jsx("rect", { x: "2", y: "7", width: "9", height: "1.5", rx: "0.5", fill: "currentColor", stroke: "none" }),
11830
+ /* @__PURE__ */ jsx("rect", { x: "2", y: "11", width: "10", height: "1.5", rx: "0.5", fill: "currentColor", stroke: "none" }),
11831
+ /* @__PURE__ */ jsx("circle", { cx: "13", cy: "11.75", r: "2.5" }),
11832
+ /* @__PURE__ */ jsx("line", { x1: "13", y1: "10.75", x2: "13", y2: "12.75", strokeWidth: "1.2" }),
11833
+ /* @__PURE__ */ jsx("line", { x1: "12", y1: "11.75", x2: "14", y2: "11.75", strokeWidth: "1.2" })
11834
+ ] });
11835
+ function RightToolbar({ watchlistOpen, onToggleWatchlist }) {
11836
+ return /* @__PURE__ */ jsx(
11837
+ "div",
11838
+ {
11839
+ style: {
11840
+ display: "flex",
11841
+ flexDirection: "column",
11842
+ alignItems: "center",
11843
+ gap: 2,
11844
+ padding: "6px 4px",
11845
+ background: "var(--toolbar-bg)",
11846
+ borderRadius: 8,
11847
+ width: 40,
11848
+ flexShrink: 0,
11849
+ userSelect: "none"
11850
+ },
11851
+ children: /* @__PURE__ */ jsx(
10623
11852
  "button",
10624
11853
  {
10625
- className: "tf-dropdown-group-hdr",
10626
- onClick: () => toggleGroup("Custom"),
10627
- children: [
10628
- /* @__PURE__ */ jsx("span", { children: "Custom" }),
10629
- /* @__PURE__ */ jsx("span", { className: `tf-dropdown-chevron${collapsed["Custom"] ? " collapsed" : ""}`, children: "\u2227" })
10630
- ]
11854
+ title: "Watch List",
11855
+ className: `drawing-tool-btn${watchlistOpen ? " is-active" : ""}`,
11856
+ onClick: onToggleWatchlist,
11857
+ children: /* @__PURE__ */ jsx(WatchListIcon, {})
10631
11858
  }
10632
- ),
10633
- !collapsed["Custom"] && customTimeframes.map((tf) => /* @__PURE__ */ jsx(
10634
- "button",
10635
- {
10636
- className: "tf-dropdown-row",
10637
- onClick: () => {
10638
- onSelect(tf);
10639
- onClose();
10640
- },
10641
- children: /* @__PURE__ */ jsx("span", { children: tf })
10642
- },
10643
- tf
10644
- ))
10645
- ] })
10646
- ] });
11859
+ )
11860
+ }
11861
+ );
11862
+ }
11863
+ var TIMEZONE_OPTIONS = [
11864
+ { label: "UTC", value: "UTC" },
11865
+ { label: "Exchange", value: "exchange" },
11866
+ // Americas
11867
+ { label: "(UTC-10) Honolulu", value: "Pacific/Honolulu" },
11868
+ { label: "(UTC-9) Anchorage", value: "America/Anchorage" },
11869
+ { label: "(UTC-9) Juneau", value: "America/Juneau" },
11870
+ { label: "(UTC-8) Los Angeles", value: "America/Los_Angeles" },
11871
+ { label: "(UTC-8) Vancouver", value: "America/Vancouver" },
11872
+ { label: "(UTC-7) Denver", value: "America/Denver" },
11873
+ { label: "(UTC-7) Phoenix", value: "America/Phoenix" },
11874
+ { label: "(UTC-6) Chicago", value: "America/Chicago" },
11875
+ { label: "(UTC-6) Mexico City", value: "America/Mexico_City" },
11876
+ { label: "(UTC-6) San Salvador", value: "America/El_Salvador" },
11877
+ { label: "(UTC-5) Bogota", value: "America/Bogota" },
11878
+ { label: "(UTC-5) Lima", value: "America/Lima" },
11879
+ { label: "(UTC-5) New York", value: "America/New_York" },
11880
+ { label: "(UTC-5) Toronto", value: "America/Toronto" },
11881
+ { label: "(UTC-4) Caracas", value: "America/Caracas" },
11882
+ { label: "(UTC-4) Halifax", value: "America/Halifax" },
11883
+ { label: "(UTC-3) Buenos Aires", value: "America/Argentina/Buenos_Aires" },
11884
+ { label: "(UTC-3) Santiago", value: "America/Santiago" },
11885
+ { label: "(UTC-3) Sao Paulo", value: "America/Sao_Paulo" },
11886
+ { label: "(UTC-2) Mid-Atlantic", value: "Atlantic/South_Georgia" },
11887
+ { label: "(UTC-1) Azores", value: "Atlantic/Azores" },
11888
+ // Europe / Africa (UTC / UTC+0)
11889
+ { label: "(UTC) Casablanca", value: "Africa/Casablanca" },
11890
+ { label: "(UTC) Dublin", value: "Europe/Dublin" },
11891
+ { label: "(UTC) Lisbon", value: "Europe/Lisbon" },
11892
+ { label: "(UTC) London", value: "Europe/London" },
11893
+ { label: "(UTC) Reykjavik", value: "Atlantic/Reykjavik" },
11894
+ // UTC+1
11895
+ { label: "(UTC+1) Amsterdam", value: "Europe/Amsterdam" },
11896
+ { label: "(UTC+1) Belgrade", value: "Europe/Belgrade" },
11897
+ { label: "(UTC+1) Berlin", value: "Europe/Berlin" },
11898
+ { label: "(UTC+1) Bratislava", value: "Europe/Bratislava" },
11899
+ { label: "(UTC+1) Brussels", value: "Europe/Brussels" },
11900
+ { label: "(UTC+1) Budapest", value: "Europe/Budapest" },
11901
+ { label: "(UTC+1) Copenhagen", value: "Europe/Copenhagen" },
11902
+ { label: "(UTC+1) Lagos", value: "Africa/Lagos" },
11903
+ { label: "(UTC+1) Luxembourg", value: "Europe/Luxembourg" },
11904
+ { label: "(UTC+1) Madrid", value: "Europe/Madrid" },
11905
+ { label: "(UTC+1) Malta", value: "Europe/Malta" },
11906
+ { label: "(UTC+1) Oslo", value: "Europe/Oslo" },
11907
+ { label: "(UTC+1) Paris", value: "Europe/Paris" },
11908
+ { label: "(UTC+1) Prague", value: "Europe/Prague" },
11909
+ { label: "(UTC+1) Rome", value: "Europe/Rome" },
11910
+ { label: "(UTC+1) Sarajevo", value: "Europe/Sarajevo" },
11911
+ { label: "(UTC+1) Stockholm", value: "Europe/Stockholm" },
11912
+ { label: "(UTC+1) Vienna", value: "Europe/Vienna" },
11913
+ { label: "(UTC+1) Warsaw", value: "Europe/Warsaw" },
11914
+ { label: "(UTC+1) Zurich", value: "Europe/Zurich" },
11915
+ // UTC+2
11916
+ { label: "(UTC+2) Athens", value: "Europe/Athens" },
11917
+ { label: "(UTC+2) Bucharest", value: "Europe/Bucharest" },
11918
+ { label: "(UTC+2) Cairo", value: "Africa/Cairo" },
11919
+ { label: "(UTC+2) Helsinki", value: "Europe/Helsinki" },
11920
+ { label: "(UTC+2) Istanbul", value: "Europe/Istanbul" },
11921
+ { label: "(UTC+2) Jerusalem", value: "Asia/Jerusalem" },
11922
+ { label: "(UTC+2) Johannesburg", value: "Africa/Johannesburg" },
11923
+ { label: "(UTC+2) Kyiv", value: "Europe/Kiev" },
11924
+ { label: "(UTC+2) Riga", value: "Europe/Riga" },
11925
+ { label: "(UTC+2) Sofia", value: "Europe/Sofia" },
11926
+ { label: "(UTC+2) Tallinn", value: "Europe/Tallinn" },
11927
+ { label: "(UTC+2) Vilnius", value: "Europe/Vilnius" },
11928
+ // UTC+3
11929
+ { label: "(UTC+3) Baghdad", value: "Asia/Baghdad" },
11930
+ { label: "(UTC+3) Kuwait", value: "Asia/Kuwait" },
11931
+ { label: "(UTC+3) Moscow", value: "Europe/Moscow" },
11932
+ { label: "(UTC+3) Nairobi", value: "Africa/Nairobi" },
11933
+ { label: "(UTC+3) Qatar", value: "Asia/Qatar" },
11934
+ { label: "(UTC+3) Riyadh", value: "Asia/Riyadh" },
11935
+ // UTC+3:30 / UTC+4
11936
+ { label: "(UTC+3:30) Tehran", value: "Asia/Tehran" },
11937
+ { label: "(UTC+4) Abu Dhabi", value: "Asia/Dubai" },
11938
+ { label: "(UTC+4) Baku", value: "Asia/Baku" },
11939
+ { label: "(UTC+4) Dubai", value: "Asia/Dubai" },
11940
+ { label: "(UTC+4) Muscat", value: "Asia/Muscat" },
11941
+ { label: "(UTC+4) Tbilisi", value: "Asia/Tbilisi" },
11942
+ { label: "(UTC+4:30) Kabul", value: "Asia/Kabul" },
11943
+ // UTC+5
11944
+ { label: "(UTC+5) Islamabad", value: "Asia/Karachi" },
11945
+ { label: "(UTC+5) Karachi", value: "Asia/Karachi" },
11946
+ { label: "(UTC+5) Tashkent", value: "Asia/Tashkent" },
11947
+ { label: "(UTC+5:30) Chennai", value: "Asia/Kolkata" },
11948
+ { label: "(UTC+5:30) Mumbai", value: "Asia/Kolkata" },
11949
+ { label: "(UTC+5:30) New Delhi", value: "Asia/Kolkata" },
11950
+ { label: "(UTC+5:45) Kathmandu", value: "Asia/Kathmandu" },
11951
+ // UTC+6
11952
+ { label: "(UTC+6) Almaty", value: "Asia/Almaty" },
11953
+ { label: "(UTC+6) Dhaka", value: "Asia/Dhaka" },
11954
+ // UTC+7
11955
+ { label: "(UTC+7) Bangkok", value: "Asia/Bangkok" },
11956
+ { label: "(UTC+7) Hanoi", value: "Asia/Ho_Chi_Minh" },
11957
+ { label: "(UTC+7) Jakarta", value: "Asia/Jakarta" },
11958
+ // UTC+8
11959
+ { label: "(UTC+8) Beijing", value: "Asia/Shanghai" },
11960
+ { label: "(UTC+8) Hong Kong", value: "Asia/Hong_Kong" },
11961
+ { label: "(UTC+8) Kuala Lumpur", value: "Asia/Kuala_Lumpur" },
11962
+ { label: "(UTC+8) Perth", value: "Australia/Perth" },
11963
+ { label: "(UTC+8) Shanghai", value: "Asia/Shanghai" },
11964
+ { label: "(UTC+8) Singapore", value: "Asia/Singapore" },
11965
+ { label: "(UTC+8) Taipei", value: "Asia/Taipei" },
11966
+ // UTC+9
11967
+ { label: "(UTC+9) Osaka", value: "Asia/Tokyo" },
11968
+ { label: "(UTC+9) Seoul", value: "Asia/Seoul" },
11969
+ { label: "(UTC+9) Tokyo", value: "Asia/Tokyo" },
11970
+ { label: "(UTC+9:30) Adelaide", value: "Australia/Adelaide" },
11971
+ { label: "(UTC+9:30) Darwin", value: "Australia/Darwin" },
11972
+ // UTC+10
11973
+ { label: "(UTC+10) Brisbane", value: "Australia/Brisbane" },
11974
+ { label: "(UTC+10) Canberra", value: "Australia/Sydney" },
11975
+ { label: "(UTC+10) Melbourne", value: "Australia/Melbourne" },
11976
+ { label: "(UTC+10) Sydney", value: "Australia/Sydney" },
11977
+ // UTC+12
11978
+ { label: "(UTC+12) Auckland", value: "Pacific/Auckland" },
11979
+ { label: "(UTC+12) Fiji", value: "Pacific/Fiji" }
11980
+ ];
11981
+ function getUtcOffsetLabel(iana) {
11982
+ try {
11983
+ const parts = new Intl.DateTimeFormat("en", {
11984
+ timeZone: iana,
11985
+ timeZoneName: "shortOffset"
11986
+ }).formatToParts(/* @__PURE__ */ new Date());
11987
+ const tzName = parts.find((p) => p.type === "timeZoneName")?.value ?? "GMT";
11988
+ const offset = tzName.replace(/^GMT/, "");
11989
+ return offset ? `(UTC${offset})` : "(UTC)";
11990
+ } catch {
11991
+ return "(UTC)";
11992
+ }
11993
+ }
11994
+ function getDisplayLabel(opt) {
11995
+ if (opt.value === "UTC" || opt.value === "exchange") return opt.label;
11996
+ const city = opt.label.replace(/^\(UTC[^)]*\)\s*/, "");
11997
+ return `${getUtcOffsetLabel(opt.value)} ${city}`;
11998
+ }
11999
+ var CRYPTO_EXCHANGES = /* @__PURE__ */ new Set([
12000
+ "BINANCE",
12001
+ "BINANCEUS",
12002
+ "BINANCEUSDM",
12003
+ "BINANCECOINM",
12004
+ "COINBASE",
12005
+ "COINBASEPRO",
12006
+ "KRAKEN",
12007
+ "KRAKENFX",
12008
+ "BYBIT",
12009
+ "BITGET",
12010
+ "OKX",
12011
+ "OKEX",
12012
+ "HUOBI",
12013
+ "HTX",
12014
+ "KUCOIN",
12015
+ "GEMINI",
12016
+ "BITSTAMP",
12017
+ "BITFINEX",
12018
+ "MEXC",
12019
+ "GATEIO",
12020
+ "GATE",
12021
+ "BITMEX",
12022
+ "DERIBIT"
12023
+ ]);
12024
+ function symbolSupportsSession(symbol) {
12025
+ const exchange = symbol.includes(":") ? symbol.split(":")[0].toUpperCase() : "";
12026
+ return exchange !== "" && !CRYPTO_EXCHANGES.has(exchange);
12027
+ }
12028
+ function useClockTime(timezone) {
12029
+ const [time, setTime] = useState(() => _formatClock(timezone));
12030
+ useEffect(() => {
12031
+ setTime(_formatClock(timezone));
12032
+ const id = setInterval(() => setTime(_formatClock(timezone)), 1e3);
12033
+ return () => clearInterval(id);
12034
+ }, [timezone]);
12035
+ return time;
12036
+ }
12037
+ function _formatClock(timezone) {
12038
+ const tz = timezone === "exchange" ? "UTC" : timezone;
12039
+ try {
12040
+ return new Intl.DateTimeFormat("en-GB", {
12041
+ timeZone: tz,
12042
+ hour: "2-digit",
12043
+ minute: "2-digit",
12044
+ second: "2-digit",
12045
+ hour12: false
12046
+ }).format(/* @__PURE__ */ new Date());
12047
+ } catch {
12048
+ return new Intl.DateTimeFormat("en-GB", {
12049
+ timeZone: "UTC",
12050
+ hour: "2-digit",
12051
+ minute: "2-digit",
12052
+ second: "2-digit",
12053
+ hour12: false
12054
+ }).format(/* @__PURE__ */ new Date());
12055
+ }
10647
12056
  }
10648
- function TopToolbar({
10649
- symbol,
10650
- timeframe,
10651
- theme,
10652
- customTimeframes,
10653
- favorites,
10654
- onSymbolChange,
10655
- onTimeframeChange,
10656
- onAddCustomTimeframe,
10657
- onFavoritesChange,
10658
- onAddIndicator,
10659
- onToggleTheme,
10660
- onCopyScreenshot,
10661
- onDownloadScreenshot,
10662
- onFullscreen,
10663
- isFullscreen,
10664
- currentLayoutName,
10665
- currentLayoutId,
10666
- autoSave,
10667
- onSaveLayout,
10668
- onLoadLayout,
10669
- onRenameLayout,
10670
- onCopyLayout,
10671
- onToggleAutoSave,
10672
- onDeleteLayout,
10673
- onOpenLayoutInNewTab,
10674
- onFetchLayouts,
10675
- symbolResolver,
10676
- showTradeButton,
10677
- tradeDrawerOpen,
10678
- onToggleTradeDrawer
10679
- }) {
10680
- const [tfOpen, setTfOpen] = useState(false);
10681
- const [symOpen, setSymOpen] = useState(false);
10682
- const [indOpen, setIndOpen] = useState(false);
10683
- const [ssOpen, setSsOpen] = useState(false);
10684
- const ssRef = useRef(null);
10685
- const tfRef = useRef(null);
12057
+ function BottomToolbar({ symbol, timezone, onTimezoneChange, session, onSessionChange, onToggleScriptDrawer, scriptDrawerOpen }) {
12058
+ const showSession = symbolSupportsSession(symbol);
12059
+ const [open, setOpen] = useState(false);
12060
+ const [sessionOpen, setSessionOpen] = useState(false);
12061
+ const rootRef = useRef(null);
12062
+ const sessionRef = useRef(null);
12063
+ const clockTime = useClockTime(timezone);
12064
+ const current = TIMEZONE_OPTIONS.find((o) => o.value === timezone) ?? TIMEZONE_OPTIONS[0];
10686
12065
  useEffect(() => {
10687
- if (!ssOpen) return;
12066
+ if (!open) return;
10688
12067
  const handler = (e) => {
10689
- if (ssRef.current && !ssRef.current.contains(e.target)) setSsOpen(false);
12068
+ if (rootRef.current && !rootRef.current.contains(e.target)) setOpen(false);
10690
12069
  };
10691
12070
  document.addEventListener("mousedown", handler);
10692
12071
  return () => document.removeEventListener("mousedown", handler);
10693
- }, [ssOpen]);
10694
- return /* @__PURE__ */ jsxs(
10695
- "div",
10696
- {
10697
- style: {
10698
- display: "flex",
10699
- alignItems: "center",
10700
- gap: 8,
10701
- padding: "6px 12px",
10702
- background: "var(--toolbar-bg)",
10703
- borderRadius: 8,
10704
- flexShrink: 0
10705
- },
10706
- children: [
10707
- /* @__PURE__ */ jsx("span", { style: { fontWeight: 700, letterSpacing: "-0.5px", color: "var(--accent)" }, children: "ForgeCharts" }),
10708
- /* @__PURE__ */ jsx("div", { className: "toolbar-sep" }),
12072
+ }, [open]);
12073
+ useEffect(() => {
12074
+ if (!sessionOpen) return;
12075
+ const handler = (e) => {
12076
+ if (sessionRef.current && !sessionRef.current.contains(e.target)) setSessionOpen(false);
12077
+ };
12078
+ document.addEventListener("mousedown", handler);
12079
+ return () => document.removeEventListener("mousedown", handler);
12080
+ }, [sessionOpen]);
12081
+ return /* @__PURE__ */ jsxs("div", { className: "bottom-bar", children: [
12082
+ /* @__PURE__ */ jsx("div", { className: "bottom-bar-left", children: /* @__PURE__ */ jsxs(
12083
+ "button",
12084
+ {
12085
+ className: `bottom-bar-btn${scriptDrawerOpen ? " active" : ""}`,
12086
+ onClick: onToggleScriptDrawer,
12087
+ title: "Script Engine",
12088
+ children: [
12089
+ /* @__PURE__ */ jsxs(
12090
+ "svg",
12091
+ {
12092
+ viewBox: "0 0 16 16",
12093
+ width: "11",
12094
+ height: "11",
12095
+ fill: "none",
12096
+ stroke: "currentColor",
12097
+ strokeWidth: "1.6",
12098
+ strokeLinecap: "round",
12099
+ children: [
12100
+ /* @__PURE__ */ jsx("polyline", { points: "4,6 2,8 4,10" }),
12101
+ /* @__PURE__ */ jsx("polyline", { points: "12,6 14,8 12,10" }),
12102
+ /* @__PURE__ */ jsx("line", { x1: "9", y1: "3", x2: "7", y2: "13" })
12103
+ ]
12104
+ }
12105
+ ),
12106
+ "Script"
12107
+ ]
12108
+ }
12109
+ ) }),
12110
+ /* @__PURE__ */ jsxs("div", { className: "bottom-bar-right", children: [
12111
+ /* @__PURE__ */ jsxs("div", { ref: rootRef, style: { position: "relative", display: "flex", alignItems: "center" }, children: [
10709
12112
  /* @__PURE__ */ jsxs(
10710
12113
  "button",
10711
12114
  {
10712
- className: "sym-trigger",
10713
- onClick: () => setSymOpen(true),
10714
- title: "Search symbols",
12115
+ className: `bottom-bar-btn${open ? " active" : ""}`,
12116
+ onClick: () => setOpen((o) => !o),
12117
+ title: "Chart timezone",
10715
12118
  children: [
10716
- /* @__PURE__ */ jsxs(
10717
- "svg",
10718
- {
10719
- viewBox: "0 0 14 14",
10720
- width: "12",
10721
- height: "12",
10722
- stroke: "currentColor",
10723
- fill: "none",
10724
- strokeWidth: "1.8",
10725
- strokeLinecap: "round",
10726
- children: [
10727
- /* @__PURE__ */ jsx("circle", { cx: "5.5", cy: "5.5", r: "4" }),
10728
- /* @__PURE__ */ jsx("line", { x1: "9", y1: "9", x2: "13", y2: "13" })
10729
- ]
10730
- }
10731
- ),
10732
- /* @__PURE__ */ jsx("span", { children: symbol }),
10733
- /* @__PURE__ */ jsx("svg", { viewBox: "0 0 10 6", width: "8", height: "6", fill: "currentColor", children: /* @__PURE__ */ jsx("path", { d: "M0 0l5 6 5-6z" }) })
12119
+ /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 14 14", width: "11", height: "11", fill: "none", stroke: "currentColor", strokeWidth: "1.2", children: [
12120
+ /* @__PURE__ */ jsx("circle", { cx: "7", cy: "7", r: "5.5" }),
12121
+ /* @__PURE__ */ jsx("path", { d: "M7 1.5C7 1.5 9 4 9 7s-2 5.5-2 5.5M7 1.5C7 1.5 5 4 5 7s2 5.5 2 5.5" }),
12122
+ /* @__PURE__ */ jsx("line", { x1: "1.5", y1: "7", x2: "12.5", y2: "7" }),
12123
+ /* @__PURE__ */ jsx("line", { x1: "2", y1: "4.5", x2: "12", y2: "4.5" }),
12124
+ /* @__PURE__ */ jsx("line", { x1: "2", y1: "9.5", x2: "12", y2: "9.5" })
12125
+ ] }),
12126
+ getDisplayLabel(current)
10734
12127
  ]
10735
12128
  }
10736
12129
  ),
10737
- symOpen && /* @__PURE__ */ jsx(
10738
- SymbolSearchDialog,
10739
- {
10740
- current: symbol,
10741
- onSelect: onSymbolChange,
10742
- onClose: () => setSymOpen(false),
10743
- symbolResolver
10744
- }
10745
- ),
10746
- /* @__PURE__ */ jsx("div", { className: "toolbar-sep" }),
10747
- /* @__PURE__ */ jsxs("div", { ref: tfRef, style: { display: "flex", gap: 2, position: "relative" }, children: [
10748
- favorites.map((tf) => /* @__PURE__ */ jsx(
12130
+ /* @__PURE__ */ jsx("span", { className: "bottom-bar-clock", title: `Current time in ${getDisplayLabel(current)}`, children: clockTime }),
12131
+ open && /* @__PURE__ */ jsxs("div", { className: "bottom-bar-dropdown bottom-bar-dropdown--right", children: [
12132
+ /* @__PURE__ */ jsx("div", { className: "bottom-bar-dropdown-section", children: "Timezone" }),
12133
+ TIMEZONE_OPTIONS.map((opt) => /* @__PURE__ */ jsxs(
10749
12134
  "button",
10750
12135
  {
10751
- className: tf === timeframe ? "active" : void 0,
10752
- onClick: () => onTimeframeChange(tf),
10753
- children: tf
12136
+ className: `bottom-bar-dropdown-item${opt.value === timezone ? " current" : ""}`,
12137
+ onClick: () => {
12138
+ onTimezoneChange(opt.value);
12139
+ setOpen(false);
12140
+ },
12141
+ children: [
12142
+ opt.value === timezone && /* @__PURE__ */ jsx("span", { className: "bottom-bar-check", children: "\xE2\u0153\u201C" }),
12143
+ getDisplayLabel(opt)
12144
+ ]
10754
12145
  },
10755
- tf
10756
- )),
10757
- !favorites.includes(timeframe) && timeframe && /* @__PURE__ */ jsx("button", { className: "active", style: { fontStyle: "italic" }, children: timeframe }),
10758
- /* @__PURE__ */ jsx(
12146
+ opt.value + opt.label
12147
+ ))
12148
+ ] })
12149
+ ] }),
12150
+ showSession && /* @__PURE__ */ jsxs("div", { ref: sessionRef, style: { position: "relative", display: "flex", alignItems: "center" }, children: [
12151
+ /* @__PURE__ */ jsxs(
12152
+ "button",
12153
+ {
12154
+ className: `bottom-bar-btn${sessionOpen ? " active" : ""}`,
12155
+ onClick: () => setSessionOpen((o) => !o),
12156
+ title: "Trading session",
12157
+ children: [
12158
+ /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 14 14", width: "11", height: "11", fill: "none", stroke: "currentColor", strokeWidth: "1.2", children: [
12159
+ /* @__PURE__ */ jsx("rect", { x: "1.5", y: "2.5", width: "11", height: "9", rx: "1" }),
12160
+ /* @__PURE__ */ jsx("line", { x1: "1.5", y1: "5.5", x2: "12.5", y2: "5.5" }),
12161
+ /* @__PURE__ */ jsx("line", { x1: "4.5", y1: "2.5", x2: "4.5", y2: "1" }),
12162
+ /* @__PURE__ */ jsx("line", { x1: "9.5", y1: "2.5", x2: "9.5", y2: "1" })
12163
+ ] }),
12164
+ session === "extended" ? "Extended hours" : "Regular hours"
12165
+ ]
12166
+ }
12167
+ ),
12168
+ sessionOpen && /* @__PURE__ */ jsxs("div", { className: "bottom-bar-dropdown bottom-bar-dropdown--right", children: [
12169
+ /* @__PURE__ */ jsx("div", { className: "bottom-bar-dropdown-section", children: "Sessions" }),
12170
+ /* @__PURE__ */ jsxs(
10759
12171
  "button",
10760
12172
  {
10761
- className: tfOpen ? "active" : void 0,
10762
- onClick: () => setTfOpen((o) => !o),
10763
- title: "More timeframes",
10764
- style: { fontSize: 11, padding: "4px 7px" },
10765
- children: "\u25BE"
12173
+ className: `bottom-bar-dropdown-item${session === "regular" ? " current" : ""}`,
12174
+ onClick: () => {
12175
+ onSessionChange("regular");
12176
+ setSessionOpen(false);
12177
+ },
12178
+ children: [
12179
+ session === "regular" && /* @__PURE__ */ jsx("span", { className: "bottom-bar-check", children: "\xE2\u0153\u201C" }),
12180
+ /* @__PURE__ */ jsxs("span", { className: "bottom-bar-session-label", children: [
12181
+ /* @__PURE__ */ jsx("strong", { children: "Regular trading hours" }),
12182
+ /* @__PURE__ */ jsx("span", { children: "Exchange session only (RTH) \xE2\u20AC\u201D cleaner signals, higher liquidity" })
12183
+ ] })
12184
+ ]
10766
12185
  }
10767
12186
  ),
10768
- tfOpen && /* @__PURE__ */ jsx(
10769
- TimeframeDropdown,
12187
+ /* @__PURE__ */ jsxs(
12188
+ "button",
10770
12189
  {
10771
- timeframe,
10772
- customTimeframes,
10773
- favorites,
10774
- onSelect: onTimeframeChange,
10775
- onToggleFavorite: (tf) => {
10776
- const next = favorites.includes(tf) ? favorites.filter((f) => f !== tf) : [...favorites, tf];
10777
- onFavoritesChange(next);
12190
+ className: `bottom-bar-dropdown-item${session === "extended" ? " current" : ""}`,
12191
+ onClick: () => {
12192
+ onSessionChange("extended");
12193
+ setSessionOpen(false);
10778
12194
  },
10779
- onAddCustom: onAddCustomTimeframe,
10780
- onClose: () => setTfOpen(false)
12195
+ children: [
12196
+ session === "extended" && /* @__PURE__ */ jsx("span", { className: "bottom-bar-check", children: "\xE2\u0153\u201C" }),
12197
+ /* @__PURE__ */ jsxs("span", { className: "bottom-bar-session-label", children: [
12198
+ /* @__PURE__ */ jsx("strong", { children: "Extended / electronic hours" }),
12199
+ /* @__PURE__ */ jsx("span", { children: "Includes pre-market & after-hours (ETH) \xE2\u20AC\u201D full price range" })
12200
+ ] })
12201
+ ]
10781
12202
  }
10782
12203
  )
12204
+ ] })
12205
+ ] })
12206
+ ] })
12207
+ ] });
12208
+ }
12209
+ function ChartWorkspace({
12210
+ // TabBar
12211
+ tabs,
12212
+ activeTabId,
12213
+ onSelectTab,
12214
+ onAddTab,
12215
+ onCloseTab,
12216
+ onRenameTab,
12217
+ // TopToolbar
12218
+ symbol,
12219
+ timeframe,
12220
+ theme,
12221
+ customTimeframes,
12222
+ favoriteTfs,
12223
+ onSymbolChange,
12224
+ onTimeframeChange,
12225
+ onAddCustomTimeframe,
12226
+ onFavoriteTfsChange,
12227
+ onAddIndicator,
12228
+ onToggleTheme,
12229
+ onCopyScreenshot,
12230
+ onDownloadScreenshot,
12231
+ onFullscreen,
12232
+ isFullscreen,
12233
+ currentLayoutName,
12234
+ currentLayoutId,
12235
+ autoSave,
12236
+ onFetchLayouts,
12237
+ onSaveLayout,
12238
+ onLoadLayout,
12239
+ onRenameLayout,
12240
+ onCopyLayout,
12241
+ onToggleAutoSave,
12242
+ onDeleteLayout,
12243
+ onOpenLayoutInNewTab,
12244
+ showTradeButton,
12245
+ tradeDrawerOpen,
12246
+ onToggleTradeDrawer,
12247
+ symbolResolver,
12248
+ // RightToolbar
12249
+ watchlistOpen,
12250
+ onToggleWatchlist,
12251
+ // BottomToolbar
12252
+ activeSymbol,
12253
+ timezone,
12254
+ onTimezoneChange,
12255
+ session,
12256
+ onSessionChange,
12257
+ scriptDrawerOpen,
12258
+ onToggleScriptDrawer,
12259
+ // Chart + state
12260
+ chartSlots,
12261
+ isLicensed,
12262
+ wsLoaded,
12263
+ drawers
12264
+ }) {
12265
+ const tabItems = tabs.map((t) => ({
12266
+ id: t.id,
12267
+ label: t.label,
12268
+ ...t.isSaved !== void 0 ? { isSaved: t.isSaved } : {}
12269
+ }));
12270
+ return /* @__PURE__ */ jsxs(
12271
+ "div",
12272
+ {
12273
+ style: {
12274
+ display: "flex",
12275
+ flexDirection: "column",
12276
+ height: "100%",
12277
+ background: "var(--shelf-bg)",
12278
+ padding: 6,
12279
+ gap: 4
12280
+ },
12281
+ children: [
12282
+ !wsLoaded && /* @__PURE__ */ jsxs("div", { style: {
12283
+ position: "absolute",
12284
+ inset: 0,
12285
+ display: "flex",
12286
+ alignItems: "center",
12287
+ justifyContent: "center",
12288
+ background: "var(--bg)",
12289
+ zIndex: 1e3
12290
+ }, children: [
12291
+ /* @__PURE__ */ jsxs("svg", { width: "32", height: "32", viewBox: "0 0 32 32", style: { animation: "spin 0.9s linear infinite" }, children: [
12292
+ /* @__PURE__ */ jsx("circle", { cx: "16", cy: "16", r: "12", fill: "none", stroke: "var(--border)", strokeWidth: "3" }),
12293
+ /* @__PURE__ */ jsx("path", { d: "M16 4 A12 12 0 0 1 28 16", fill: "none", stroke: "var(--primary)", strokeWidth: "3", strokeLinecap: "round" })
12294
+ ] }),
12295
+ /* @__PURE__ */ jsx("style", { children: `@keyframes spin { to { transform: rotate(360deg); } }` })
10783
12296
  ] }),
10784
- /* @__PURE__ */ jsx("div", { className: "toolbar-sep" }),
10785
- /* @__PURE__ */ jsxs(
10786
- "button",
12297
+ /* @__PURE__ */ jsx(
12298
+ TabBar,
10787
12299
  {
10788
- className: `ind-trigger${indOpen ? " active" : ""}`,
10789
- onClick: () => setIndOpen((o) => !o),
10790
- title: "Indicators",
10791
- children: [
10792
- /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 16 16", width: "13", height: "13", stroke: "currentColor", fill: "none", strokeWidth: "1.6", strokeLinecap: "round", children: [
10793
- /* @__PURE__ */ jsx("polyline", { points: "2,12 6,7 9,10 14,4" }),
10794
- /* @__PURE__ */ jsx("line", { x1: "11", y1: "2", x2: "14", y2: "2" }),
10795
- /* @__PURE__ */ jsx("line", { x1: "14", y1: "2", x2: "14", y2: "5" })
10796
- ] }),
10797
- "Indicators"
10798
- ]
12300
+ tabs: tabItems,
12301
+ activeId: activeTabId,
12302
+ onSelect: onSelectTab,
12303
+ onAdd: onAddTab,
12304
+ onClose: onCloseTab,
12305
+ onRename: onRenameTab
10799
12306
  }
10800
12307
  ),
10801
- indOpen && /* @__PURE__ */ jsx(
10802
- IndicatorsDialog,
12308
+ /* @__PURE__ */ jsx(
12309
+ TopToolbar,
10803
12310
  {
10804
- onAdd: (config) => {
10805
- onAddIndicator(config);
10806
- },
10807
- onClose: () => setIndOpen(false)
12311
+ symbol,
12312
+ timeframe,
12313
+ theme,
12314
+ customTimeframes,
12315
+ favorites: favoriteTfs,
12316
+ onSymbolChange,
12317
+ onTimeframeChange,
12318
+ onAddCustomTimeframe,
12319
+ onFavoritesChange: onFavoriteTfsChange,
12320
+ onAddIndicator,
12321
+ onToggleTheme,
12322
+ onCopyScreenshot,
12323
+ onDownloadScreenshot,
12324
+ onFullscreen,
12325
+ isFullscreen,
12326
+ currentLayoutName,
12327
+ currentLayoutId,
12328
+ autoSave,
12329
+ onFetchLayouts,
12330
+ onSaveLayout,
12331
+ onLoadLayout,
12332
+ onRenameLayout,
12333
+ onCopyLayout,
12334
+ onToggleAutoSave,
12335
+ onDeleteLayout,
12336
+ onOpenLayoutInNewTab,
12337
+ symbolResolver,
12338
+ ...showTradeButton !== void 0 ? { showTradeButton } : {},
12339
+ ...tradeDrawerOpen !== void 0 ? { tradeDrawerOpen } : {},
12340
+ ...onToggleTradeDrawer !== void 0 ? { onToggleTradeDrawer } : {}
10808
12341
  }
10809
12342
  ),
10810
- /* @__PURE__ */ jsxs("div", { style: { marginLeft: "auto", display: "flex", alignItems: "center", gap: 4 }, children: [
10811
- showTradeButton && /* @__PURE__ */ jsxs(Fragment, { children: [
10812
- /* @__PURE__ */ jsxs(
10813
- "button",
10814
- {
10815
- className: `top-trade-btn${tradeDrawerOpen ? " active" : ""}`,
10816
- onClick: onToggleTradeDrawer,
10817
- title: "Trade \u2014 Connect your broker",
10818
- style: { display: "inline-flex", alignItems: "center", gap: 5, padding: "4px 8px", fontSize: 12 },
10819
- children: [
10820
- /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 16 16", width: "11", height: "11", fill: "none", stroke: "currentColor", strokeWidth: "1.6", children: [
10821
- /* @__PURE__ */ jsx("rect", { x: "1.5", y: "3", width: "13", height: "10", rx: "1.5" }),
10822
- /* @__PURE__ */ jsx("line", { x1: "5", y1: "8", x2: "11", y2: "8" }),
10823
- /* @__PURE__ */ jsx("line", { x1: "8", y1: "5", x2: "8", y2: "11" })
10824
- ] }),
10825
- "Trade"
10826
- ]
10827
- }
10828
- ),
10829
- /* @__PURE__ */ jsx("div", { className: "toolbar-sep" })
10830
- ] }),
10831
- /* @__PURE__ */ jsx(
10832
- LayoutMenu,
10833
- {
10834
- currentName: currentLayoutName,
10835
- currentLayoutId,
10836
- autoSave,
10837
- onFetchLayouts,
10838
- onSave: onSaveLayout,
10839
- onLoad: onLoadLayout,
10840
- onRename: onRenameLayout,
10841
- onCopy: onCopyLayout,
10842
- onToggleAutoSave,
10843
- onDelete: onDeleteLayout,
10844
- onOpenInNewTab: onOpenLayoutInNewTab
10845
- }
10846
- ),
10847
- /* @__PURE__ */ jsx(
10848
- "button",
10849
- {
10850
- onClick: onToggleTheme,
10851
- title: `Switch to ${theme === "dark" ? "light" : "dark"} mode`,
10852
- style: { fontSize: 14, padding: "4px 8px" },
10853
- children: theme === "dark" ? "\u2600" : "\u263E"
10854
- }
10855
- ),
10856
- /* @__PURE__ */ jsxs("div", { ref: ssRef, style: { position: "relative" }, children: [
12343
+ /* @__PURE__ */ jsxs("div", { style: { flex: 1, display: "flex", gap: 8, minHeight: 0, overflow: "hidden" }, children: [
12344
+ /* @__PURE__ */ jsxs("div", { style: { position: "relative", flex: 1, display: "flex", gap: 8, minWidth: 0, minHeight: 0 }, children: [
12345
+ !isLicensed && /* @__PURE__ */ jsxs("div", { style: {
12346
+ position: "absolute",
12347
+ inset: 0,
12348
+ zIndex: 100,
12349
+ background: "rgba(0,0,0,0.6)",
12350
+ backdropFilter: "blur(4px)",
12351
+ display: "flex",
12352
+ flexDirection: "column",
12353
+ alignItems: "center",
12354
+ justifyContent: "center",
12355
+ gap: 12
12356
+ }, children: [
12357
+ /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 24 24", width: "40", height: "40", fill: "none", stroke: "rgba(255,255,255,0.7)", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round", children: [
12358
+ /* @__PURE__ */ jsx("rect", { x: "3", y: "11", width: "18", height: "11", rx: "2" }),
12359
+ /* @__PURE__ */ jsx("path", { d: "M7 11V7a5 5 0 0110 0v4" })
12360
+ ] }),
12361
+ /* @__PURE__ */ jsx("div", { style: { color: "#fff", fontWeight: 700, fontSize: 18, letterSpacing: "0.01em" }, children: "Unlicensed" }),
12362
+ /* @__PURE__ */ jsx("div", { style: { color: "rgba(255,255,255,0.65)", fontSize: 13 }, children: "Please activate your license in the admin studio" })
12363
+ ] }),
12364
+ /* @__PURE__ */ jsx("div", { style: { position: "relative", flex: 1, minWidth: 0, minHeight: 0 }, children: chartSlots }),
10857
12365
  /* @__PURE__ */ jsx(
10858
- "button",
12366
+ RightToolbar,
10859
12367
  {
10860
- className: ssOpen ? "active" : void 0,
10861
- onClick: () => setSsOpen((o) => !o),
10862
- title: "Screenshot",
10863
- style: { display: "inline-flex", alignItems: "center", padding: "4px 7px" },
10864
- children: /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 16 16", width: "14", height: "14", stroke: "currentColor", fill: "none", strokeWidth: "1.5", children: [
10865
- /* @__PURE__ */ jsx("rect", { x: "2", y: "4", width: "12", height: "9", rx: "1" }),
10866
- /* @__PURE__ */ jsx("circle", { cx: "8", cy: "8.5", r: "2.5" }),
10867
- /* @__PURE__ */ jsx("path", { d: "M6 4l1-2h2l1 2" })
10868
- ] })
12368
+ watchlistOpen,
12369
+ onToggleWatchlist
10869
12370
  }
10870
- ),
10871
- ssOpen && /* @__PURE__ */ jsxs("div", { className: "ss-dropdown", children: [
10872
- /* @__PURE__ */ jsxs("button", { className: "ss-dropdown-item", onClick: () => {
10873
- setSsOpen(false);
10874
- onCopyScreenshot();
10875
- }, children: [
10876
- /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 16 16", width: "13", height: "13", stroke: "currentColor", fill: "none", strokeWidth: "1.5", children: [
10877
- /* @__PURE__ */ jsx("rect", { x: "5", y: "5", width: "9", height: "9", rx: "1" }),
10878
- /* @__PURE__ */ jsx("path", { d: "M3 11H2a1 1 0 01-1-1V2a1 1 0 011-1h8a1 1 0 011 1v1" })
10879
- ] }),
10880
- "Copy to Clipboard"
10881
- ] }),
10882
- /* @__PURE__ */ jsxs("button", { className: "ss-dropdown-item", onClick: () => {
10883
- setSsOpen(false);
10884
- onDownloadScreenshot();
10885
- }, children: [
10886
- /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 16 16", width: "13", height: "13", stroke: "currentColor", fill: "none", strokeWidth: "1.5", strokeLinecap: "round", children: [
10887
- /* @__PURE__ */ jsx("line", { x1: "8", y1: "2", x2: "8", y2: "11" }),
10888
- /* @__PURE__ */ jsx("polyline", { points: "4,7 8,11 12,7" }),
10889
- /* @__PURE__ */ jsx("line", { x1: "2", y1: "14", x2: "14", y2: "14" })
10890
- ] }),
10891
- "Download PNG"
10892
- ] })
10893
- ] })
12371
+ )
10894
12372
  ] }),
10895
- /* @__PURE__ */ jsx(
10896
- "button",
10897
- {
10898
- onClick: onFullscreen,
10899
- title: isFullscreen ? "Exit Fullscreen" : "Fullscreen",
10900
- style: { display: "inline-flex", alignItems: "center", padding: "4px 7px" },
10901
- children: isFullscreen ? /* @__PURE__ */ jsx("svg", { viewBox: "0 0 16 16", width: "14", height: "14", stroke: "currentColor", fill: "none", strokeWidth: "1.5", children: /* @__PURE__ */ jsx("path", { d: "M6 2v4H2M10 2v4h4M10 14v-4h4M6 14v-4H2" }) }) : /* @__PURE__ */ jsx("svg", { viewBox: "0 0 16 16", width: "14", height: "14", stroke: "currentColor", fill: "none", strokeWidth: "1.5", children: /* @__PURE__ */ jsx("path", { d: "M2 6V2h4M10 2h4v4M14 10v4h-4M6 14H2v-4" }) })
10902
- }
10903
- )
10904
- ] })
12373
+ drawers
12374
+ ] }),
12375
+ /* @__PURE__ */ jsx(
12376
+ BottomToolbar,
12377
+ {
12378
+ symbol: activeSymbol,
12379
+ timezone,
12380
+ onTimezoneChange,
12381
+ session,
12382
+ onSessionChange,
12383
+ onToggleScriptDrawer,
12384
+ scriptDrawerOpen
12385
+ }
12386
+ )
10905
12387
  ]
10906
12388
  }
10907
12389
  );
10908
12390
  }
10909
- var WatchListIcon = () => /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 16 16", width: "16", height: "16", stroke: "currentColor", fill: "none", strokeWidth: "1.5", children: [
10910
- /* @__PURE__ */ jsx("rect", { x: "2", y: "3", width: "12", height: "1.5", rx: "0.5", fill: "currentColor", stroke: "none" }),
10911
- /* @__PURE__ */ jsx("rect", { x: "2", y: "7", width: "9", height: "1.5", rx: "0.5", fill: "currentColor", stroke: "none" }),
10912
- /* @__PURE__ */ jsx("rect", { x: "2", y: "11", width: "10", height: "1.5", rx: "0.5", fill: "currentColor", stroke: "none" }),
10913
- /* @__PURE__ */ jsx("circle", { cx: "13", cy: "11.75", r: "2.5" }),
10914
- /* @__PURE__ */ jsx("line", { x1: "13", y1: "10.75", x2: "13", y2: "12.75", strokeWidth: "1.2" }),
10915
- /* @__PURE__ */ jsx("line", { x1: "12", y1: "11.75", x2: "14", y2: "11.75", strokeWidth: "1.2" })
10916
- ] });
10917
- function RightToolbar({ watchlistOpen, onToggleWatchlist }) {
10918
- return /* @__PURE__ */ jsx(
10919
- "div",
10920
- {
10921
- style: {
10922
- display: "flex",
10923
- flexDirection: "column",
10924
- alignItems: "center",
10925
- gap: 2,
10926
- padding: "6px 4px",
10927
- background: "var(--toolbar-bg)",
10928
- borderRadius: 8,
10929
- width: 40,
10930
- flexShrink: 0,
10931
- userSelect: "none"
10932
- },
10933
- children: /* @__PURE__ */ jsx(
10934
- "button",
10935
- {
10936
- title: "Watch List",
10937
- className: `drawing-tool-btn${watchlistOpen ? " is-active" : ""}`,
10938
- onClick: onToggleWatchlist,
10939
- children: /* @__PURE__ */ jsx(WatchListIcon, {})
10940
- }
10941
- )
10942
- }
10943
- );
10944
- }
10945
- var TIMEZONE_OPTIONS = [
10946
- { label: "UTC", value: "UTC" },
10947
- { label: "Exchange", value: "exchange" },
10948
- // Americas
10949
- { label: "(UTC-10) Honolulu", value: "Pacific/Honolulu" },
10950
- { label: "(UTC-9) Anchorage", value: "America/Anchorage" },
10951
- { label: "(UTC-9) Juneau", value: "America/Juneau" },
10952
- { label: "(UTC-8) Los Angeles", value: "America/Los_Angeles" },
10953
- { label: "(UTC-8) Vancouver", value: "America/Vancouver" },
10954
- { label: "(UTC-7) Denver", value: "America/Denver" },
10955
- { label: "(UTC-7) Phoenix", value: "America/Phoenix" },
10956
- { label: "(UTC-6) Chicago", value: "America/Chicago" },
10957
- { label: "(UTC-6) Mexico City", value: "America/Mexico_City" },
10958
- { label: "(UTC-6) San Salvador", value: "America/El_Salvador" },
10959
- { label: "(UTC-5) Bogota", value: "America/Bogota" },
10960
- { label: "(UTC-5) Lima", value: "America/Lima" },
10961
- { label: "(UTC-5) New York", value: "America/New_York" },
10962
- { label: "(UTC-5) Toronto", value: "America/Toronto" },
10963
- { label: "(UTC-4) Caracas", value: "America/Caracas" },
10964
- { label: "(UTC-4) Halifax", value: "America/Halifax" },
10965
- { label: "(UTC-3) Buenos Aires", value: "America/Argentina/Buenos_Aires" },
10966
- { label: "(UTC-3) Santiago", value: "America/Santiago" },
10967
- { label: "(UTC-3) Sao Paulo", value: "America/Sao_Paulo" },
10968
- { label: "(UTC-2) Mid-Atlantic", value: "Atlantic/South_Georgia" },
10969
- { label: "(UTC-1) Azores", value: "Atlantic/Azores" },
10970
- // Europe / Africa (UTC / UTC+0)
10971
- { label: "(UTC) Casablanca", value: "Africa/Casablanca" },
10972
- { label: "(UTC) Dublin", value: "Europe/Dublin" },
10973
- { label: "(UTC) Lisbon", value: "Europe/Lisbon" },
10974
- { label: "(UTC) London", value: "Europe/London" },
10975
- { label: "(UTC) Reykjavik", value: "Atlantic/Reykjavik" },
10976
- // UTC+1
10977
- { label: "(UTC+1) Amsterdam", value: "Europe/Amsterdam" },
10978
- { label: "(UTC+1) Belgrade", value: "Europe/Belgrade" },
10979
- { label: "(UTC+1) Berlin", value: "Europe/Berlin" },
10980
- { label: "(UTC+1) Bratislava", value: "Europe/Bratislava" },
10981
- { label: "(UTC+1) Brussels", value: "Europe/Brussels" },
10982
- { label: "(UTC+1) Budapest", value: "Europe/Budapest" },
10983
- { label: "(UTC+1) Copenhagen", value: "Europe/Copenhagen" },
10984
- { label: "(UTC+1) Lagos", value: "Africa/Lagos" },
10985
- { label: "(UTC+1) Luxembourg", value: "Europe/Luxembourg" },
10986
- { label: "(UTC+1) Madrid", value: "Europe/Madrid" },
10987
- { label: "(UTC+1) Malta", value: "Europe/Malta" },
10988
- { label: "(UTC+1) Oslo", value: "Europe/Oslo" },
10989
- { label: "(UTC+1) Paris", value: "Europe/Paris" },
10990
- { label: "(UTC+1) Prague", value: "Europe/Prague" },
10991
- { label: "(UTC+1) Rome", value: "Europe/Rome" },
10992
- { label: "(UTC+1) Sarajevo", value: "Europe/Sarajevo" },
10993
- { label: "(UTC+1) Stockholm", value: "Europe/Stockholm" },
10994
- { label: "(UTC+1) Vienna", value: "Europe/Vienna" },
10995
- { label: "(UTC+1) Warsaw", value: "Europe/Warsaw" },
10996
- { label: "(UTC+1) Zurich", value: "Europe/Zurich" },
10997
- // UTC+2
10998
- { label: "(UTC+2) Athens", value: "Europe/Athens" },
10999
- { label: "(UTC+2) Bucharest", value: "Europe/Bucharest" },
11000
- { label: "(UTC+2) Cairo", value: "Africa/Cairo" },
11001
- { label: "(UTC+2) Helsinki", value: "Europe/Helsinki" },
11002
- { label: "(UTC+2) Istanbul", value: "Europe/Istanbul" },
11003
- { label: "(UTC+2) Jerusalem", value: "Asia/Jerusalem" },
11004
- { label: "(UTC+2) Johannesburg", value: "Africa/Johannesburg" },
11005
- { label: "(UTC+2) Kyiv", value: "Europe/Kiev" },
11006
- { label: "(UTC+2) Riga", value: "Europe/Riga" },
11007
- { label: "(UTC+2) Sofia", value: "Europe/Sofia" },
11008
- { label: "(UTC+2) Tallinn", value: "Europe/Tallinn" },
11009
- { label: "(UTC+2) Vilnius", value: "Europe/Vilnius" },
11010
- // UTC+3
11011
- { label: "(UTC+3) Baghdad", value: "Asia/Baghdad" },
11012
- { label: "(UTC+3) Kuwait", value: "Asia/Kuwait" },
11013
- { label: "(UTC+3) Moscow", value: "Europe/Moscow" },
11014
- { label: "(UTC+3) Nairobi", value: "Africa/Nairobi" },
11015
- { label: "(UTC+3) Qatar", value: "Asia/Qatar" },
11016
- { label: "(UTC+3) Riyadh", value: "Asia/Riyadh" },
11017
- // UTC+3:30 / UTC+4
11018
- { label: "(UTC+3:30) Tehran", value: "Asia/Tehran" },
11019
- { label: "(UTC+4) Abu Dhabi", value: "Asia/Dubai" },
11020
- { label: "(UTC+4) Baku", value: "Asia/Baku" },
11021
- { label: "(UTC+4) Dubai", value: "Asia/Dubai" },
11022
- { label: "(UTC+4) Muscat", value: "Asia/Muscat" },
11023
- { label: "(UTC+4) Tbilisi", value: "Asia/Tbilisi" },
11024
- { label: "(UTC+4:30) Kabul", value: "Asia/Kabul" },
11025
- // UTC+5
11026
- { label: "(UTC+5) Islamabad", value: "Asia/Karachi" },
11027
- { label: "(UTC+5) Karachi", value: "Asia/Karachi" },
11028
- { label: "(UTC+5) Tashkent", value: "Asia/Tashkent" },
11029
- { label: "(UTC+5:30) Chennai", value: "Asia/Kolkata" },
11030
- { label: "(UTC+5:30) Mumbai", value: "Asia/Kolkata" },
11031
- { label: "(UTC+5:30) New Delhi", value: "Asia/Kolkata" },
11032
- { label: "(UTC+5:45) Kathmandu", value: "Asia/Kathmandu" },
11033
- // UTC+6
11034
- { label: "(UTC+6) Almaty", value: "Asia/Almaty" },
11035
- { label: "(UTC+6) Dhaka", value: "Asia/Dhaka" },
11036
- // UTC+7
11037
- { label: "(UTC+7) Bangkok", value: "Asia/Bangkok" },
11038
- { label: "(UTC+7) Hanoi", value: "Asia/Ho_Chi_Minh" },
11039
- { label: "(UTC+7) Jakarta", value: "Asia/Jakarta" },
11040
- // UTC+8
11041
- { label: "(UTC+8) Beijing", value: "Asia/Shanghai" },
11042
- { label: "(UTC+8) Hong Kong", value: "Asia/Hong_Kong" },
11043
- { label: "(UTC+8) Kuala Lumpur", value: "Asia/Kuala_Lumpur" },
11044
- { label: "(UTC+8) Perth", value: "Australia/Perth" },
11045
- { label: "(UTC+8) Shanghai", value: "Asia/Shanghai" },
11046
- { label: "(UTC+8) Singapore", value: "Asia/Singapore" },
11047
- { label: "(UTC+8) Taipei", value: "Asia/Taipei" },
11048
- // UTC+9
11049
- { label: "(UTC+9) Osaka", value: "Asia/Tokyo" },
11050
- { label: "(UTC+9) Seoul", value: "Asia/Seoul" },
11051
- { label: "(UTC+9) Tokyo", value: "Asia/Tokyo" },
11052
- { label: "(UTC+9:30) Adelaide", value: "Australia/Adelaide" },
11053
- { label: "(UTC+9:30) Darwin", value: "Australia/Darwin" },
11054
- // UTC+10
11055
- { label: "(UTC+10) Brisbane", value: "Australia/Brisbane" },
11056
- { label: "(UTC+10) Canberra", value: "Australia/Sydney" },
11057
- { label: "(UTC+10) Melbourne", value: "Australia/Melbourne" },
11058
- { label: "(UTC+10) Sydney", value: "Australia/Sydney" },
11059
- // UTC+12
11060
- { label: "(UTC+12) Auckland", value: "Pacific/Auckland" },
11061
- { label: "(UTC+12) Fiji", value: "Pacific/Fiji" }
11062
- ];
11063
- function getUtcOffsetLabel(iana) {
11064
- try {
11065
- const parts = new Intl.DateTimeFormat("en", {
11066
- timeZone: iana,
11067
- timeZoneName: "shortOffset"
11068
- }).formatToParts(/* @__PURE__ */ new Date());
11069
- const tzName = parts.find((p) => p.type === "timeZoneName")?.value ?? "GMT";
11070
- const offset = tzName.replace(/^GMT/, "");
11071
- return offset ? `(UTC${offset})` : "(UTC)";
11072
- } catch {
11073
- return "(UTC)";
11074
- }
11075
- }
11076
- function getDisplayLabel(opt) {
11077
- if (opt.value === "UTC" || opt.value === "exchange") return opt.label;
11078
- const city = opt.label.replace(/^\(UTC[^)]*\)\s*/, "");
11079
- return `${getUtcOffsetLabel(opt.value)} ${city}`;
11080
- }
11081
- var CRYPTO_EXCHANGES = /* @__PURE__ */ new Set([
11082
- "BINANCE",
11083
- "BINANCEUS",
11084
- "BINANCEUSDM",
11085
- "BINANCECOINM",
11086
- "COINBASE",
11087
- "COINBASEPRO",
11088
- "KRAKEN",
11089
- "KRAKENFX",
11090
- "BYBIT",
11091
- "BITGET",
11092
- "OKX",
11093
- "OKEX",
11094
- "HUOBI",
11095
- "HTX",
11096
- "KUCOIN",
11097
- "GEMINI",
11098
- "BITSTAMP",
11099
- "BITFINEX",
11100
- "MEXC",
11101
- "GATEIO",
11102
- "GATE",
11103
- "BITMEX",
11104
- "DERIBIT"
11105
- ]);
11106
- function symbolSupportsSession(symbol) {
11107
- const exchange = symbol.includes(":") ? symbol.split(":")[0].toUpperCase() : "";
11108
- return exchange !== "" && !CRYPTO_EXCHANGES.has(exchange);
11109
- }
11110
- function useClockTime(timezone) {
11111
- const [time, setTime] = useState(() => _formatClock(timezone));
11112
- useEffect(() => {
11113
- setTime(_formatClock(timezone));
11114
- const id = setInterval(() => setTime(_formatClock(timezone)), 1e3);
11115
- return () => clearInterval(id);
11116
- }, [timezone]);
11117
- return time;
11118
- }
11119
- function _formatClock(timezone) {
11120
- const tz = timezone === "exchange" ? "UTC" : timezone;
11121
- try {
11122
- return new Intl.DateTimeFormat("en-GB", {
11123
- timeZone: tz,
11124
- hour: "2-digit",
11125
- minute: "2-digit",
11126
- second: "2-digit",
11127
- hour12: false
11128
- }).format(/* @__PURE__ */ new Date());
11129
- } catch {
11130
- return new Intl.DateTimeFormat("en-GB", {
11131
- timeZone: "UTC",
11132
- hour: "2-digit",
11133
- minute: "2-digit",
11134
- second: "2-digit",
11135
- hour12: false
11136
- }).format(/* @__PURE__ */ new Date());
11137
- }
12391
+ function applyRuntimeConfig(caps, config) {
12392
+ if (!config) return caps;
12393
+ const orderEntry = config.enableOrderEntry === false ? false : caps.orderEntry;
12394
+ const draggableOrders = config.enableDraggableOrders === false ? false : caps.draggableOrders;
12395
+ const bracketOrders = config.enableBracketOrders === false ? false : caps.bracketOrders;
12396
+ const managedTrading = config.enableManagedTrading === false ? false : caps.managedTrading;
12397
+ const indicators = config.enableIndicators === false ? false : caps.indicators;
12398
+ return {
12399
+ ...caps,
12400
+ orderEntry,
12401
+ draggableOrders,
12402
+ bracketOrders,
12403
+ managedTrading,
12404
+ indicators,
12405
+ // Recalculate derived render flags from the updated feature flags.
12406
+ renderOrderEntry: orderEntry,
12407
+ renderBuySellButtons: orderEntry,
12408
+ renderOrderTicket: orderEntry,
12409
+ renderBracketControls: orderEntry && bracketOrders,
12410
+ renderOrderModificationControls: orderEntry && draggableOrders,
12411
+ renderManagedTradingControls: managedTrading,
12412
+ renderIndicators: indicators
12413
+ };
11138
12414
  }
11139
- function BottomToolbar({ symbol, timezone, onTimezoneChange, session, onSessionChange, onToggleScriptDrawer, scriptDrawerOpen }) {
11140
- const showSession = symbolSupportsSession(symbol);
11141
- const [open, setOpen] = useState(false);
11142
- const [sessionOpen, setSessionOpen] = useState(false);
11143
- const rootRef = useRef(null);
11144
- const sessionRef = useRef(null);
11145
- const clockTime = useClockTime(timezone);
11146
- const current = TIMEZONE_OPTIONS.find((o) => o.value === timezone) ?? TIMEZONE_OPTIONS[0];
12415
+ function useChartCapabilities(config) {
12416
+ const [licCaps, setLicCaps] = useState(() => getCapabilities());
11147
12417
  useEffect(() => {
11148
- if (!open) return;
11149
- const handler = (e) => {
11150
- if (rootRef.current && !rootRef.current.contains(e.target)) setOpen(false);
12418
+ const unsub = LicenseManager.getInstance().subscribe(() => {
12419
+ setLicCaps(getCapabilities());
12420
+ });
12421
+ return unsub;
12422
+ }, []);
12423
+ return useMemo(() => applyRuntimeConfig(licCaps, config), [licCaps, config]);
12424
+ }
12425
+
12426
+ // src/react/trading/TradingBridge.ts
12427
+ function createTradingBridgeLogger(callbacks, label) {
12428
+ if (process.env.NODE_ENV === "production") return callbacks;
12429
+ const tag = label ? `[TradingBridge:${label}]` : "[TradingBridge]";
12430
+ return {
12431
+ onPlaceOrderIntent: (intent) => {
12432
+ console.debug(
12433
+ `${tag} PLACE side=%s type=%s qty=%s price=%s stopPrice=%s correlationId=%s`,
12434
+ intent.side,
12435
+ intent.orderType,
12436
+ intent.qty,
12437
+ intent.limitPrice ?? "\u2014",
12438
+ intent.stopPrice ?? "\u2014",
12439
+ intent.correlationId,
12440
+ intent
12441
+ );
12442
+ return callbacks.onPlaceOrderIntent?.(intent);
12443
+ },
12444
+ onModifyOrderIntent: (intent) => {
12445
+ console.debug(
12446
+ `${tag} MODIFY orderId=%s limitPrice=%s stopPrice=%s qty=%s`,
12447
+ intent.orderId,
12448
+ intent.limitPrice ?? "\u2014",
12449
+ intent.stopPrice ?? "\u2014",
12450
+ intent.qty ?? "\u2014",
12451
+ intent
12452
+ );
12453
+ return callbacks.onModifyOrderIntent?.(intent);
12454
+ },
12455
+ onCancelOrderIntent: (intent) => {
12456
+ console.debug(
12457
+ `${tag} CANCEL orderId=%s symbol=%s`,
12458
+ intent.orderId,
12459
+ intent.symbol,
12460
+ intent
12461
+ );
12462
+ return callbacks.onCancelOrderIntent?.(intent);
12463
+ },
12464
+ onBracketAdjustIntent: (intent) => {
12465
+ console.debug(
12466
+ `${tag} BRACKET_ADJUST entry=%s sl=%s tp=%s`,
12467
+ intent.entryOrderId,
12468
+ intent.stopLossPrice ?? "\u2014",
12469
+ intent.takeProfitPrice ?? "\u2014",
12470
+ intent
12471
+ );
12472
+ return callbacks.onBracketAdjustIntent?.(intent);
12473
+ }
12474
+ };
12475
+ }
12476
+ var TA_FUNCTIONS = [
12477
+ { name: "sma(src, len)", desc: "Simple moving average" },
12478
+ { name: "ema(src, len)", desc: "Exponential moving average" },
12479
+ { name: "wma(src, len)", desc: "Weighted moving average" },
12480
+ { name: "rma(src, len)", desc: "Wilder (RMA) moving average" },
12481
+ { name: "stdev(src, len)", desc: "Standard deviation" },
12482
+ { name: "highest(src, len)", desc: "Highest value over N bars" },
12483
+ { name: "lowest(src, len)", desc: "Lowest value over N bars" },
12484
+ { name: "atr(len)", desc: "Average true range" },
12485
+ { name: "change(src)", desc: "Difference from previous bar" },
12486
+ { name: "mom(src, len)", desc: "Momentum" },
12487
+ { name: "crossover(a, b)", desc: "True when a crosses above b" },
12488
+ { name: "crossunder(a, b)", desc: "True when a crosses below b" },
12489
+ { name: "barssince(cond)", desc: "Bars since condition was true" }
12490
+ ];
12491
+ var BUILT_IN_SERIES = [
12492
+ "open",
12493
+ "high",
12494
+ "low",
12495
+ "close",
12496
+ "volume",
12497
+ "hl2",
12498
+ "hlc3",
12499
+ "ohlc4",
12500
+ "bar_index"
12501
+ ];
12502
+ var UTILITY_FUNCTIONS = [
12503
+ { name: 'indicator("Title")', desc: "Declare the indicator name" },
12504
+ { name: "input(default)", desc: "Create a user-configurable parameter" },
12505
+ { name: "plot(value)", desc: "Plot a line on the chart" },
12506
+ { name: "nz(val, rep?)", desc: "Replace NaN with rep (default 0)" },
12507
+ { name: "na(val)", desc: "Returns true if value is NaN" }
12508
+ ];
12509
+ var MATH_FUNCTIONS = ["abs", "max", "min", "round", "floor", "ceil", "sqrt", "log", "pow", "sign"];
12510
+ var TSCRIPT_TEMPLATE = `indicator("My Indicator")
12511
+
12512
+ // Parameters \u2014 shown in the UI
12513
+ length = input(14)
12514
+ fast = input(9)
12515
+ slow = input(21)
12516
+
12517
+ // Calculations using built-in TA functions
12518
+ fast_ma = ema(close, fast)
12519
+ slow_ma = ema(close, slow)
12520
+ signal = crossover(fast_ma, slow_ma)
12521
+
12522
+ // Plot results (overlay: true draws on price pane)
12523
+ plot(fast_ma)
12524
+ plot(slow_ma)
12525
+ `;
12526
+ var PINE_TEMPLATE = `//@version=5
12527
+ indicator("My Pine Indicator", overlay=false)
12528
+
12529
+ length = input.int(14, title="Length")
12530
+ src = input.source(close, title="Source")
12531
+
12532
+ value = ta.rsi(src, length)
12533
+
12534
+ plot(value, title="RSI", color=color.blue)
12535
+ hline(70, "Overbought", color=color.red)
12536
+ hline(30, "Oversold", color=color.green)
12537
+ `;
12538
+ function ScriptDrawer({ onClose, onAddIndicator }) {
12539
+ const [lang, setLang] = useState("tscript");
12540
+ const [code, setCode] = useState(TSCRIPT_TEMPLATE);
12541
+ const [overlay, setOverlay] = useState(false);
12542
+ const [errors, setErrors] = useState([]);
12543
+ const [refOpen, setRefOpen] = useState(false);
12544
+ const [drawerWidth, setDrawerWidth] = useState(380);
12545
+ const dragState = useRef(null);
12546
+ const onResizeMouseDown = (e) => {
12547
+ e.preventDefault();
12548
+ dragState.current = { startX: e.clientX, startW: drawerWidth };
12549
+ const onMove = (ev) => {
12550
+ if (!dragState.current) return;
12551
+ const delta = dragState.current.startX - ev.clientX;
12552
+ setDrawerWidth(Math.min(700, Math.max(280, dragState.current.startW + delta)));
11151
12553
  };
11152
- document.addEventListener("mousedown", handler);
11153
- return () => document.removeEventListener("mousedown", handler);
11154
- }, [open]);
11155
- useEffect(() => {
11156
- if (!sessionOpen) return;
11157
- const handler = (e) => {
11158
- if (sessionRef.current && !sessionRef.current.contains(e.target)) setSessionOpen(false);
12554
+ const onUp = () => {
12555
+ dragState.current = null;
12556
+ window.removeEventListener("mousemove", onMove);
12557
+ window.removeEventListener("mouseup", onUp);
11159
12558
  };
11160
- document.addEventListener("mousedown", handler);
11161
- return () => document.removeEventListener("mousedown", handler);
11162
- }, [sessionOpen]);
11163
- return /* @__PURE__ */ jsxs("div", { className: "bottom-bar", children: [
11164
- /* @__PURE__ */ jsx("div", { className: "bottom-bar-left", children: /* @__PURE__ */ jsxs(
11165
- "button",
11166
- {
11167
- className: `bottom-bar-btn${scriptDrawerOpen ? " active" : ""}`,
11168
- onClick: onToggleScriptDrawer,
11169
- title: "Script Engine",
11170
- children: [
11171
- /* @__PURE__ */ jsxs(
11172
- "svg",
11173
- {
11174
- viewBox: "0 0 16 16",
11175
- width: "11",
11176
- height: "11",
11177
- fill: "none",
11178
- stroke: "currentColor",
11179
- strokeWidth: "1.6",
11180
- strokeLinecap: "round",
11181
- children: [
11182
- /* @__PURE__ */ jsx("polyline", { points: "4,6 2,8 4,10" }),
11183
- /* @__PURE__ */ jsx("polyline", { points: "12,6 14,8 12,10" }),
11184
- /* @__PURE__ */ jsx("line", { x1: "9", y1: "3", x2: "7", y2: "13" })
11185
- ]
11186
- }
11187
- ),
11188
- "Script"
11189
- ]
12559
+ window.addEventListener("mousemove", onMove);
12560
+ window.addEventListener("mouseup", onUp);
12561
+ };
12562
+ const handleLangChange = (next) => {
12563
+ setLang(next);
12564
+ setErrors([]);
12565
+ setCode(next === "tscript" ? TSCRIPT_TEMPLATE : PINE_TEMPLATE);
12566
+ };
12567
+ const handleAddToChart = () => {
12568
+ setErrors([]);
12569
+ if (lang === "pine") {
12570
+ const result = new PineCompiler().compile(code);
12571
+ if (!result.ok) {
12572
+ const msgs = result.diagnostics.map(
12573
+ (d) => `Line ${d.line ?? "?"}: ${d.message}`
12574
+ );
12575
+ setErrors(msgs.length ? msgs : ["Compilation failed \u2014 check your Pine Script."]);
12576
+ return;
11190
12577
  }
11191
- ) }),
11192
- /* @__PURE__ */ jsxs("div", { className: "bottom-bar-right", children: [
11193
- /* @__PURE__ */ jsxs("div", { ref: rootRef, style: { position: "relative", display: "flex", alignItems: "center" }, children: [
12578
+ onAddIndicator({ type: "script", script: result.tscript, overlay });
12579
+ } else {
12580
+ onAddIndicator({ type: "script", script: code, overlay });
12581
+ }
12582
+ };
12583
+ return /* @__PURE__ */ jsxs("div", { className: "script-drawer", style: { width: drawerWidth }, children: [
12584
+ /* @__PURE__ */ jsx("div", { className: "script-drawer-resize", onMouseDown: onResizeMouseDown }),
12585
+ /* @__PURE__ */ jsxs("div", { className: "script-drawer-header", children: [
12586
+ /* @__PURE__ */ jsxs("span", { className: "script-drawer-title", children: [
11194
12587
  /* @__PURE__ */ jsxs(
11195
- "button",
12588
+ "svg",
11196
12589
  {
11197
- className: `bottom-bar-btn${open ? " active" : ""}`,
11198
- onClick: () => setOpen((o) => !o),
11199
- title: "Chart timezone",
12590
+ viewBox: "0 0 16 16",
12591
+ width: "14",
12592
+ height: "14",
12593
+ fill: "none",
12594
+ stroke: "currentColor",
12595
+ strokeWidth: "1.6",
12596
+ strokeLinecap: "round",
12597
+ style: { flexShrink: 0 },
11200
12598
  children: [
11201
- /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 14 14", width: "11", height: "11", fill: "none", stroke: "currentColor", strokeWidth: "1.2", children: [
11202
- /* @__PURE__ */ jsx("circle", { cx: "7", cy: "7", r: "5.5" }),
11203
- /* @__PURE__ */ jsx("path", { d: "M7 1.5C7 1.5 9 4 9 7s-2 5.5-2 5.5M7 1.5C7 1.5 5 4 5 7s2 5.5 2 5.5" }),
11204
- /* @__PURE__ */ jsx("line", { x1: "1.5", y1: "7", x2: "12.5", y2: "7" }),
11205
- /* @__PURE__ */ jsx("line", { x1: "2", y1: "4.5", x2: "12", y2: "4.5" }),
11206
- /* @__PURE__ */ jsx("line", { x1: "2", y1: "9.5", x2: "12", y2: "9.5" })
11207
- ] }),
11208
- getDisplayLabel(current)
12599
+ /* @__PURE__ */ jsx("polyline", { points: "4,6 2,8 4,10" }),
12600
+ /* @__PURE__ */ jsx("polyline", { points: "12,6 14,8 12,10" }),
12601
+ /* @__PURE__ */ jsx("line", { x1: "9", y1: "3", x2: "7", y2: "13" })
11209
12602
  ]
11210
12603
  }
11211
12604
  ),
11212
- /* @__PURE__ */ jsx("span", { className: "bottom-bar-clock", title: `Current time in ${getDisplayLabel(current)}`, children: clockTime }),
11213
- open && /* @__PURE__ */ jsxs("div", { className: "bottom-bar-dropdown bottom-bar-dropdown--right", children: [
11214
- /* @__PURE__ */ jsx("div", { className: "bottom-bar-dropdown-section", children: "Timezone" }),
11215
- TIMEZONE_OPTIONS.map((opt) => /* @__PURE__ */ jsxs(
11216
- "button",
11217
- {
11218
- className: `bottom-bar-dropdown-item${opt.value === timezone ? " current" : ""}`,
11219
- onClick: () => {
11220
- onTimezoneChange(opt.value);
11221
- setOpen(false);
11222
- },
11223
- children: [
11224
- opt.value === timezone && /* @__PURE__ */ jsx("span", { className: "bottom-bar-check", children: "\xE2\u0153\u201C" }),
11225
- getDisplayLabel(opt)
11226
- ]
11227
- },
11228
- opt.value + opt.label
11229
- ))
11230
- ] })
12605
+ "Script Engine"
11231
12606
  ] }),
11232
- showSession && /* @__PURE__ */ jsxs("div", { ref: sessionRef, style: { position: "relative", display: "flex", alignItems: "center" }, children: [
11233
- /* @__PURE__ */ jsxs(
11234
- "button",
11235
- {
11236
- className: `bottom-bar-btn${sessionOpen ? " active" : ""}`,
11237
- onClick: () => setSessionOpen((o) => !o),
11238
- title: "Trading session",
11239
- children: [
11240
- /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 14 14", width: "11", height: "11", fill: "none", stroke: "currentColor", strokeWidth: "1.2", children: [
11241
- /* @__PURE__ */ jsx("rect", { x: "1.5", y: "2.5", width: "11", height: "9", rx: "1" }),
11242
- /* @__PURE__ */ jsx("line", { x1: "1.5", y1: "5.5", x2: "12.5", y2: "5.5" }),
11243
- /* @__PURE__ */ jsx("line", { x1: "4.5", y1: "2.5", x2: "4.5", y2: "1" }),
11244
- /* @__PURE__ */ jsx("line", { x1: "9.5", y1: "2.5", x2: "9.5", y2: "1" })
11245
- ] }),
11246
- session === "extended" ? "Extended hours" : "Regular hours"
11247
- ]
11248
- }
11249
- ),
11250
- sessionOpen && /* @__PURE__ */ jsxs("div", { className: "bottom-bar-dropdown bottom-bar-dropdown--right", children: [
11251
- /* @__PURE__ */ jsx("div", { className: "bottom-bar-dropdown-section", children: "Sessions" }),
11252
- /* @__PURE__ */ jsxs(
11253
- "button",
11254
- {
11255
- className: `bottom-bar-dropdown-item${session === "regular" ? " current" : ""}`,
11256
- onClick: () => {
11257
- onSessionChange("regular");
11258
- setSessionOpen(false);
11259
- },
11260
- children: [
11261
- session === "regular" && /* @__PURE__ */ jsx("span", { className: "bottom-bar-check", children: "\xE2\u0153\u201C" }),
11262
- /* @__PURE__ */ jsxs("span", { className: "bottom-bar-session-label", children: [
11263
- /* @__PURE__ */ jsx("strong", { children: "Regular trading hours" }),
11264
- /* @__PURE__ */ jsx("span", { children: "Exchange session only (RTH) \xE2\u20AC\u201D cleaner signals, higher liquidity" })
11265
- ] })
11266
- ]
11267
- }
11268
- ),
11269
- /* @__PURE__ */ jsxs(
11270
- "button",
11271
- {
11272
- className: `bottom-bar-dropdown-item${session === "extended" ? " current" : ""}`,
11273
- onClick: () => {
11274
- onSessionChange("extended");
11275
- setSessionOpen(false);
11276
- },
11277
- children: [
11278
- session === "extended" && /* @__PURE__ */ jsx("span", { className: "bottom-bar-check", children: "\xE2\u0153\u201C" }),
11279
- /* @__PURE__ */ jsxs("span", { className: "bottom-bar-session-label", children: [
11280
- /* @__PURE__ */ jsx("strong", { children: "Extended / electronic hours" }),
11281
- /* @__PURE__ */ jsx("span", { children: "Includes pre-market & after-hours (ETH) \xE2\u20AC\u201D full price range" })
11282
- ] })
11283
- ]
11284
- }
11285
- )
11286
- ] })
12607
+ /* @__PURE__ */ jsx("button", { className: "script-close-btn", onClick: onClose, title: "Close", children: /* @__PURE__ */ jsxs(
12608
+ "svg",
12609
+ {
12610
+ viewBox: "0 0 12 12",
12611
+ width: "12",
12612
+ height: "12",
12613
+ stroke: "currentColor",
12614
+ strokeWidth: "1.8",
12615
+ strokeLinecap: "round",
12616
+ children: [
12617
+ /* @__PURE__ */ jsx("line", { x1: "1", y1: "1", x2: "11", y2: "11" }),
12618
+ /* @__PURE__ */ jsx("line", { x1: "11", y1: "1", x2: "1", y2: "11" })
12619
+ ]
12620
+ }
12621
+ ) })
12622
+ ] }),
12623
+ /* @__PURE__ */ jsxs("div", { className: "script-lang-tabs", children: [
12624
+ /* @__PURE__ */ jsx(
12625
+ "button",
12626
+ {
12627
+ className: `script-lang-tab${lang === "tscript" ? " active" : ""}`,
12628
+ onClick: () => handleLangChange("tscript"),
12629
+ children: "TScript"
12630
+ }
12631
+ ),
12632
+ /* @__PURE__ */ jsx(
12633
+ "button",
12634
+ {
12635
+ className: `script-lang-tab${lang === "pine" ? " active" : ""}`,
12636
+ onClick: () => handleLangChange("pine"),
12637
+ children: "Pine Script\u2122"
12638
+ }
12639
+ )
12640
+ ] }),
12641
+ /* @__PURE__ */ jsxs("div", { className: "script-ref-section", children: [
12642
+ /* @__PURE__ */ jsxs(
12643
+ "button",
12644
+ {
12645
+ className: "script-ref-toggle",
12646
+ onClick: () => setRefOpen((o) => !o),
12647
+ children: [
12648
+ /* @__PURE__ */ jsx(
12649
+ "svg",
12650
+ {
12651
+ viewBox: "0 0 10 6",
12652
+ width: "8",
12653
+ height: "6",
12654
+ fill: "currentColor",
12655
+ style: { transform: refOpen ? "rotate(180deg)" : "none", transition: "transform 0.15s" },
12656
+ children: /* @__PURE__ */ jsx("path", { d: "M0 0l5 6 5-6z" })
12657
+ }
12658
+ ),
12659
+ "Built-in Reference"
12660
+ ]
12661
+ }
12662
+ ),
12663
+ refOpen && /* @__PURE__ */ jsxs("div", { className: "script-ref-body", children: [
12664
+ /* @__PURE__ */ jsx("div", { className: "script-ref-group-label", children: "Series" }),
12665
+ /* @__PURE__ */ jsx("div", { className: "script-feature-list", children: BUILT_IN_SERIES.map((s) => /* @__PURE__ */ jsx("span", { className: "script-feature-pill series", children: s }, s)) }),
12666
+ /* @__PURE__ */ jsx("div", { className: "script-ref-group-label", children: "TA Functions" }),
12667
+ /* @__PURE__ */ jsx("div", { className: "script-feature-list", children: TA_FUNCTIONS.map((f) => /* @__PURE__ */ jsxs("span", { className: "script-feature-pill ta", title: f.desc, children: [
12668
+ f.name.split("(")[0],
12669
+ "()"
12670
+ ] }, f.name)) }),
12671
+ /* @__PURE__ */ jsx("div", { className: "script-ref-group-label", children: "Math" }),
12672
+ /* @__PURE__ */ jsx("div", { className: "script-feature-list", children: MATH_FUNCTIONS.map((f) => /* @__PURE__ */ jsxs("span", { className: "script-feature-pill math", children: [
12673
+ f,
12674
+ "()"
12675
+ ] }, f)) }),
12676
+ /* @__PURE__ */ jsx("div", { className: "script-ref-group-label", children: "Utility" }),
12677
+ /* @__PURE__ */ jsx("div", { className: "script-feature-list", children: UTILITY_FUNCTIONS.map((f) => /* @__PURE__ */ jsxs("span", { className: "script-feature-pill util", title: f.desc, children: [
12678
+ f.name.split("(")[0],
12679
+ "()"
12680
+ ] }, f.name)) }),
12681
+ /* @__PURE__ */ jsx("div", { className: "script-ref-detail", children: [...TA_FUNCTIONS, ...UTILITY_FUNCTIONS].map((f) => /* @__PURE__ */ jsxs("div", { className: "script-ref-row", children: [
12682
+ /* @__PURE__ */ jsx("code", { className: "script-ref-fn", children: f.name }),
12683
+ /* @__PURE__ */ jsx("span", { className: "script-ref-desc", children: f.desc })
12684
+ ] }, f.name)) })
12685
+ ] })
12686
+ ] }),
12687
+ /* @__PURE__ */ jsx("div", { className: "script-editor-wrap", children: /* @__PURE__ */ jsx(
12688
+ "textarea",
12689
+ {
12690
+ className: "script-editor",
12691
+ spellCheck: false,
12692
+ value: code,
12693
+ onChange: (e) => {
12694
+ setCode(e.target.value);
12695
+ setErrors([]);
12696
+ }
12697
+ }
12698
+ ) }),
12699
+ errors.length > 0 && /* @__PURE__ */ jsx("div", { className: "script-errors", children: errors.map((msg, i) => /* @__PURE__ */ jsxs("div", { className: "script-error-line", children: [
12700
+ /* @__PURE__ */ jsxs(
12701
+ "svg",
12702
+ {
12703
+ viewBox: "0 0 12 12",
12704
+ width: "11",
12705
+ height: "11",
12706
+ fill: "none",
12707
+ stroke: "currentColor",
12708
+ strokeWidth: "1.6",
12709
+ strokeLinecap: "round",
12710
+ style: { flexShrink: 0, color: "var(--down, #ef5350)" },
12711
+ children: [
12712
+ /* @__PURE__ */ jsx("circle", { cx: "6", cy: "6", r: "5" }),
12713
+ /* @__PURE__ */ jsx("line", { x1: "6", y1: "3.5", x2: "6", y2: "6.5" }),
12714
+ /* @__PURE__ */ jsx("line", { x1: "6", y1: "8", x2: "6", y2: "8.5" })
12715
+ ]
12716
+ }
12717
+ ),
12718
+ msg
12719
+ ] }, i)) }),
12720
+ /* @__PURE__ */ jsxs("div", { className: "script-footer", children: [
12721
+ /* @__PURE__ */ jsxs("label", { className: "script-overlay-label", children: [
12722
+ /* @__PURE__ */ jsx(
12723
+ "input",
12724
+ {
12725
+ type: "checkbox",
12726
+ checked: overlay,
12727
+ onChange: (e) => setOverlay(e.target.checked)
12728
+ }
12729
+ ),
12730
+ "Overlay on price"
12731
+ ] }),
12732
+ /* @__PURE__ */ jsxs("button", { className: "script-run-btn", onClick: handleAddToChart, children: [
12733
+ /* @__PURE__ */ jsx(
12734
+ "svg",
12735
+ {
12736
+ viewBox: "0 0 12 12",
12737
+ width: "11",
12738
+ height: "11",
12739
+ fill: "currentColor",
12740
+ style: { flexShrink: 0 },
12741
+ children: /* @__PURE__ */ jsx("polygon", { points: "2,1 11,6 2,11" })
12742
+ }
12743
+ ),
12744
+ "Add to Chart"
11287
12745
  ] })
11288
12746
  ] })
11289
12747
  ] });
11290
12748
  }
11291
- function ChartWorkspace({
11292
- // TabBar
11293
- tabs,
11294
- activeTabId,
11295
- onSelectTab,
11296
- onAddTab,
11297
- onCloseTab,
11298
- onRenameTab,
11299
- // TopToolbar
11300
- symbol,
11301
- timeframe,
11302
- theme,
11303
- customTimeframes,
11304
- favoriteTfs,
11305
- onSymbolChange,
11306
- onTimeframeChange,
11307
- onAddCustomTimeframe,
11308
- onFavoriteTfsChange,
11309
- onAddIndicator,
11310
- onToggleTheme,
11311
- onCopyScreenshot,
11312
- onDownloadScreenshot,
11313
- onFullscreen,
11314
- isFullscreen,
11315
- currentLayoutName,
11316
- currentLayoutId,
11317
- autoSave,
11318
- onFetchLayouts,
11319
- onSaveLayout,
11320
- onLoadLayout,
11321
- onRenameLayout,
11322
- onCopyLayout,
11323
- onToggleAutoSave,
11324
- onDeleteLayout,
11325
- onOpenLayoutInNewTab,
11326
- showTradeButton,
11327
- tradeDrawerOpen,
11328
- onToggleTradeDrawer,
11329
- symbolResolver,
11330
- // RightToolbar
11331
- watchlistOpen,
11332
- onToggleWatchlist,
11333
- // BottomToolbar
11334
- activeSymbol,
11335
- timezone,
11336
- onTimezoneChange,
11337
- session,
11338
- onSessionChange,
11339
- scriptDrawerOpen,
11340
- onToggleScriptDrawer,
11341
- // Chart + state
11342
- chartSlots,
11343
- isLicensed,
11344
- wsLoaded,
11345
- drawers
12749
+
12750
+ // src/react/shell/watchlistApi.ts
12751
+ var _apiBase = "/api";
12752
+ var _memToken = null;
12753
+ var _getAuthToken = null;
12754
+ async function _resolveToken() {
12755
+ if (_getAuthToken) {
12756
+ try {
12757
+ return await _getAuthToken();
12758
+ } catch {
12759
+ }
12760
+ }
12761
+ if (_memToken) return _memToken;
12762
+ if (typeof localStorage !== "undefined") return localStorage.getItem("forgecharts-token");
12763
+ return null;
12764
+ }
12765
+ async function _authHeader() {
12766
+ const token = await _resolveToken();
12767
+ return token ? { Authorization: `Bearer ${token}` } : {};
12768
+ }
12769
+ async function apiFetch(method, path, body) {
12770
+ const bodyInit = body !== void 0 ? JSON.stringify(body) : null;
12771
+ const res = await fetch(`${_apiBase}${path}`, {
12772
+ method,
12773
+ headers: { "Content-Type": "application/json", ...await _authHeader() },
12774
+ ...bodyInit !== null ? { body: bodyInit } : {}
12775
+ });
12776
+ if (!res.ok) {
12777
+ const err = await res.json().catch(() => ({ error: res.statusText }));
12778
+ throw new Error(err.error ?? res.statusText);
12779
+ }
12780
+ if (res.status === 204) return void 0;
12781
+ return res.json();
12782
+ }
12783
+ var watchlistApi = {
12784
+ configure(cfg) {
12785
+ if (cfg.apiBase) _apiBase = cfg.apiBase;
12786
+ if (cfg.getAuthToken) _getAuthToken = cfg.getAuthToken;
12787
+ if (cfg.token) _memToken = cfg.token;
12788
+ },
12789
+ setToken(token) {
12790
+ _memToken = token;
12791
+ },
12792
+ getAll() {
12793
+ return apiFetch("GET", "/watchlist");
12794
+ },
12795
+ addItem(symbol, group_id) {
12796
+ return apiFetch("POST", "/watchlist/items", { symbol, group_id });
12797
+ },
12798
+ removeItem(id) {
12799
+ return apiFetch("DELETE", `/watchlist/items/${encodeURIComponent(id)}`);
12800
+ },
12801
+ moveItemToGroup(id, group_id) {
12802
+ return apiFetch("PATCH", `/watchlist/items/${encodeURIComponent(id)}`, { group_id });
12803
+ },
12804
+ addGroup(name) {
12805
+ return apiFetch("POST", "/watchlist/groups", { name });
12806
+ },
12807
+ removeGroup(id) {
12808
+ return apiFetch("DELETE", `/watchlist/groups/${encodeURIComponent(id)}`);
12809
+ },
12810
+ reorderItems(ids) {
12811
+ return apiFetch("PUT", "/watchlist/items/reorder", { ids });
12812
+ }
12813
+ };
12814
+ var _ws = null;
12815
+ var _listeners = /* @__PURE__ */ new Set();
12816
+ var _subscribed = /* @__PURE__ */ new Set();
12817
+ var _reconnectTimer = null;
12818
+ function _wsUrl() {
12819
+ if (typeof window === "undefined") return "ws://localhost:4001/ws";
12820
+ const proto = window.location.protocol === "https:" ? "wss" : "ws";
12821
+ return `${proto}://${window.location.host}/ws`;
12822
+ }
12823
+ function _send(msg) {
12824
+ if (_ws?.readyState === WebSocket.OPEN) {
12825
+ _ws.send(JSON.stringify(msg));
12826
+ }
12827
+ }
12828
+ function _ensureConnected() {
12829
+ if (_ws && (_ws.readyState === WebSocket.CONNECTING || _ws.readyState === WebSocket.OPEN)) return;
12830
+ if (_reconnectTimer) {
12831
+ clearTimeout(_reconnectTimer);
12832
+ _reconnectTimer = null;
12833
+ }
12834
+ _ws = new WebSocket(_wsUrl());
12835
+ _ws.onopen = () => {
12836
+ for (const sym of _subscribed) {
12837
+ _send({ type: "subscribe_quote", symbol: sym });
12838
+ }
12839
+ };
12840
+ _ws.onmessage = (event) => {
12841
+ try {
12842
+ const msg = JSON.parse(event.data);
12843
+ if (msg.type === "quote") {
12844
+ for (const listener of _listeners) {
12845
+ listener(msg.payload);
12846
+ }
12847
+ }
12848
+ } catch {
12849
+ }
12850
+ };
12851
+ _ws.onclose = () => {
12852
+ _ws = null;
12853
+ if (_subscribed.size > 0) {
12854
+ _reconnectTimer = setTimeout(_ensureConnected, 3e3);
12855
+ }
12856
+ };
12857
+ }
12858
+ function _addListener(fn) {
12859
+ _listeners.add(fn);
12860
+ }
12861
+ function _removeListener(fn) {
12862
+ _listeners.delete(fn);
12863
+ }
12864
+ function _subscribe(symbols) {
12865
+ _ensureConnected();
12866
+ for (const sym of symbols) {
12867
+ if (!_subscribed.has(sym)) {
12868
+ _subscribed.add(sym);
12869
+ _send({ type: "subscribe_quote", symbol: sym });
12870
+ }
12871
+ }
12872
+ }
12873
+ function _unsubscribe(symbols) {
12874
+ for (const sym of symbols) {
12875
+ _subscribed.delete(sym);
12876
+ _send({ type: "unsubscribe_quote", symbol: sym });
12877
+ }
12878
+ if (_subscribed.size === 0 && _ws) {
12879
+ _ws.close();
12880
+ _ws = null;
12881
+ }
12882
+ }
12883
+ function useWatchlistQuotes(symbols) {
12884
+ const [prices, setPrices] = useState(/* @__PURE__ */ new Map());
12885
+ const [dirs, setDirs] = useState(/* @__PURE__ */ new Map());
12886
+ const symbolsKey = symbols.slice().sort().join(",");
12887
+ const listenerRef = useRef(null);
12888
+ const onQuote = useCallback((payload) => {
12889
+ if (!_subscribed.has(payload.symbol)) return;
12890
+ setPrices((prev) => {
12891
+ const next = new Map(prev);
12892
+ next.set(payload.symbol, payload.last);
12893
+ return next;
12894
+ });
12895
+ if (payload.dir) {
12896
+ const dir = payload.dir;
12897
+ setDirs((prev) => {
12898
+ const next = new Map(prev);
12899
+ next.set(payload.symbol, dir);
12900
+ return next;
12901
+ });
12902
+ setTimeout(() => {
12903
+ setDirs((prev) => {
12904
+ const next = new Map(prev);
12905
+ next.delete(payload.symbol);
12906
+ return next;
12907
+ });
12908
+ }, 650);
12909
+ }
12910
+ }, []);
12911
+ useEffect(() => {
12912
+ if (listenerRef.current) _removeListener(listenerRef.current);
12913
+ listenerRef.current = onQuote;
12914
+ _addListener(onQuote);
12915
+ return () => {
12916
+ if (listenerRef.current) _removeListener(listenerRef.current);
12917
+ listenerRef.current = null;
12918
+ };
12919
+ }, [onQuote]);
12920
+ useEffect(() => {
12921
+ const syms = symbols.filter(Boolean);
12922
+ if (syms.length > 0) _subscribe(syms);
12923
+ return () => {
12924
+ if (syms.length > 0) _unsubscribe(syms);
12925
+ };
12926
+ }, [symbolsKey]);
12927
+ return { prices, dirs };
12928
+ }
12929
+ function fmtPrice(v) {
12930
+ const abs = Math.abs(v);
12931
+ const dec = abs >= 1e3 ? 2 : abs >= 1 ? 2 : abs >= 1e-4 ? 4 : 6;
12932
+ return v.toLocaleString("en-US", { minimumFractionDigits: dec, maximumFractionDigits: dec });
12933
+ }
12934
+ function SymbolRow({
12935
+ item,
12936
+ quote,
12937
+ flash,
12938
+ dragOver,
12939
+ logoUrl,
12940
+ onSelect,
12941
+ onRemove,
12942
+ onContextMenu,
12943
+ onDragStart,
12944
+ onDragOver,
12945
+ onDrop,
12946
+ onDragEnd
11346
12947
  }) {
11347
- const tabItems = tabs.map((t) => ({
11348
- id: t.id,
11349
- label: t.label,
11350
- ...t.isSaved !== void 0 ? { isSaved: t.isSaved } : {}
11351
- }));
12948
+ const liveColor = flash === "up" ? "var(--wl-up)" : flash === "down" ? "var(--wl-down)" : quote ? quote.change >= 0 ? "var(--wl-up)" : "var(--wl-down)" : "var(--text-muted)";
12949
+ const changeColor = quote ? quote.change >= 0 ? "var(--wl-up)" : "var(--wl-down)" : "var(--text-muted)";
11352
12950
  return /* @__PURE__ */ jsxs(
11353
12951
  "div",
11354
12952
  {
11355
- style: {
11356
- display: "flex",
11357
- flexDirection: "column",
11358
- height: "100%",
11359
- background: "var(--shelf-bg)",
11360
- padding: 6,
11361
- gap: 4
11362
- },
12953
+ className: `wl-row${dragOver ? " wl-drag-over" : ""}`,
12954
+ draggable: true,
12955
+ onDragStart,
12956
+ onDragOver,
12957
+ onDrop,
12958
+ onDragEnd,
12959
+ onClick: onSelect,
12960
+ onContextMenu,
12961
+ title: item.symbol,
11363
12962
  children: [
11364
- !wsLoaded && /* @__PURE__ */ jsxs("div", { style: {
11365
- position: "absolute",
11366
- inset: 0,
11367
- display: "flex",
11368
- alignItems: "center",
11369
- justifyContent: "center",
11370
- background: "var(--bg)",
11371
- zIndex: 1e3
11372
- }, children: [
11373
- /* @__PURE__ */ jsxs("svg", { width: "32", height: "32", viewBox: "0 0 32 32", style: { animation: "spin 0.9s linear infinite" }, children: [
11374
- /* @__PURE__ */ jsx("circle", { cx: "16", cy: "16", r: "12", fill: "none", stroke: "var(--border)", strokeWidth: "3" }),
11375
- /* @__PURE__ */ jsx("path", { d: "M16 4 A12 12 0 0 1 28 16", fill: "none", stroke: "var(--primary)", strokeWidth: "3", strokeLinecap: "round" })
11376
- ] }),
11377
- /* @__PURE__ */ jsx("style", { children: `@keyframes spin { to { transform: rotate(360deg); } }` })
11378
- ] }),
11379
- /* @__PURE__ */ jsx(
11380
- TabBar,
11381
- {
11382
- tabs: tabItems,
11383
- activeId: activeTabId,
11384
- onSelect: onSelectTab,
11385
- onAdd: onAddTab,
11386
- onClose: onCloseTab,
11387
- onRename: onRenameTab
11388
- }
11389
- ),
11390
- /* @__PURE__ */ jsx(
11391
- TopToolbar,
12963
+ /* @__PURE__ */ jsx("div", { className: "wl-drag-handle", title: "Drag to reorder", children: /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 8 12", width: "8", height: "12", fill: "currentColor", children: [
12964
+ /* @__PURE__ */ jsx("rect", { x: "0", y: "0", width: "2", height: "2", rx: "1" }),
12965
+ /* @__PURE__ */ jsx("rect", { x: "0", y: "5", width: "2", height: "2", rx: "1" }),
12966
+ /* @__PURE__ */ jsx("rect", { x: "0", y: "10", width: "2", height: "2", rx: "1" }),
12967
+ /* @__PURE__ */ jsx("rect", { x: "5", y: "0", width: "2", height: "2", rx: "1" }),
12968
+ /* @__PURE__ */ jsx("rect", { x: "5", y: "5", width: "2", height: "2", rx: "1" }),
12969
+ /* @__PURE__ */ jsx("rect", { x: "5", y: "10", width: "2", height: "2", rx: "1" })
12970
+ ] }) }),
12971
+ item.symbol.includes(":") && /* @__PURE__ */ jsx(
12972
+ "img",
11392
12973
  {
11393
- symbol,
11394
- timeframe,
11395
- theme,
11396
- customTimeframes,
11397
- favorites: favoriteTfs,
11398
- onSymbolChange,
11399
- onTimeframeChange,
11400
- onAddCustomTimeframe,
11401
- onFavoritesChange: onFavoriteTfsChange,
11402
- onAddIndicator,
11403
- onToggleTheme,
11404
- onCopyScreenshot,
11405
- onDownloadScreenshot,
11406
- onFullscreen,
11407
- isFullscreen,
11408
- currentLayoutName,
11409
- currentLayoutId,
11410
- autoSave,
11411
- onFetchLayouts,
11412
- onSaveLayout,
11413
- onLoadLayout,
11414
- onRenameLayout,
11415
- onCopyLayout,
11416
- onToggleAutoSave,
11417
- onDeleteLayout,
11418
- onOpenLayoutInNewTab,
11419
- symbolResolver,
11420
- ...showTradeButton !== void 0 ? { showTradeButton } : {},
11421
- ...tradeDrawerOpen !== void 0 ? { tradeDrawerOpen } : {},
11422
- ...onToggleTradeDrawer !== void 0 ? { onToggleTradeDrawer } : {}
12974
+ className: "wl-logo",
12975
+ src: logoUrl ?? `/api/logos/${encodeURIComponent(item.symbol.split(":")[0].toUpperCase())}`,
12976
+ alt: "",
12977
+ onError: (e) => {
12978
+ const img = e.currentTarget;
12979
+ if (logoUrl && !img.src.includes("/api/logos/")) {
12980
+ img.src = `/api/logos/${encodeURIComponent(item.symbol.split(":")[0].toUpperCase())}`;
12981
+ } else {
12982
+ img.style.display = "none";
12983
+ }
12984
+ }
11423
12985
  }
11424
12986
  ),
11425
- /* @__PURE__ */ jsxs("div", { style: { flex: 1, display: "flex", gap: 8, minHeight: 0, overflow: "hidden" }, children: [
11426
- /* @__PURE__ */ jsxs("div", { style: { position: "relative", flex: 1, display: "flex", gap: 8, minWidth: 0, minHeight: 0 }, children: [
11427
- !isLicensed && /* @__PURE__ */ jsxs("div", { style: {
11428
- position: "absolute",
11429
- inset: 0,
11430
- zIndex: 100,
11431
- background: "rgba(0,0,0,0.6)",
11432
- backdropFilter: "blur(4px)",
11433
- display: "flex",
11434
- flexDirection: "column",
11435
- alignItems: "center",
11436
- justifyContent: "center",
11437
- gap: 12
11438
- }, children: [
11439
- /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 24 24", width: "40", height: "40", fill: "none", stroke: "rgba(255,255,255,0.7)", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round", children: [
11440
- /* @__PURE__ */ jsx("rect", { x: "3", y: "11", width: "18", height: "11", rx: "2" }),
11441
- /* @__PURE__ */ jsx("path", { d: "M7 11V7a5 5 0 0110 0v4" })
11442
- ] }),
11443
- /* @__PURE__ */ jsx("div", { style: { color: "#fff", fontWeight: 700, fontSize: 18, letterSpacing: "0.01em" }, children: "Unlicensed" }),
11444
- /* @__PURE__ */ jsx("div", { style: { color: "rgba(255,255,255,0.65)", fontSize: 13 }, children: "Please activate your license in the admin studio" })
11445
- ] }),
11446
- /* @__PURE__ */ jsx("div", { style: { position: "relative", flex: 1, minWidth: 0, minHeight: 0 }, children: chartSlots }),
11447
- /* @__PURE__ */ jsx(
11448
- RightToolbar,
11449
- {
11450
- watchlistOpen,
11451
- onToggleWatchlist
11452
- }
11453
- )
12987
+ /* @__PURE__ */ jsx("span", { className: "wl-symbol", children: item.symbol }),
12988
+ quote ? /* @__PURE__ */ jsxs(Fragment, { children: [
12989
+ /* @__PURE__ */ jsx(
12990
+ "span",
12991
+ {
12992
+ className: `wl-last${flash ? ` wl-flash-${flash}` : ""}`,
12993
+ style: { color: liveColor },
12994
+ children: fmtPrice(quote.last)
12995
+ }
12996
+ ),
12997
+ /* @__PURE__ */ jsxs("span", { className: "wl-change", style: { color: changeColor }, children: [
12998
+ quote.change >= 0 ? "+" : "",
12999
+ fmtPrice(quote.change)
11454
13000
  ] }),
11455
- drawers
11456
- ] }),
13001
+ /* @__PURE__ */ jsxs("span", { className: "wl-pct", style: { color: changeColor }, children: [
13002
+ quote.change >= 0 ? "+" : "",
13003
+ quote.changePct.toFixed(2),
13004
+ "%"
13005
+ ] })
13006
+ ] }) : /* @__PURE__ */ jsx("span", { className: "wl-no-data", children: "\u2014" }),
11457
13007
  /* @__PURE__ */ jsx(
11458
- BottomToolbar,
13008
+ "button",
11459
13009
  {
11460
- symbol: activeSymbol,
11461
- timezone,
11462
- onTimezoneChange,
11463
- session,
11464
- onSessionChange,
11465
- onToggleScriptDrawer,
11466
- scriptDrawerOpen
13010
+ className: "wl-remove-btn",
13011
+ title: "Remove",
13012
+ onClick: (e) => {
13013
+ e.stopPropagation();
13014
+ onRemove();
13015
+ },
13016
+ children: "\u2715"
11467
13017
  }
11468
13018
  )
11469
13019
  ]
11470
13020
  }
11471
13021
  );
11472
13022
  }
11473
- function applyRuntimeConfig(caps, config) {
11474
- if (!config) return caps;
11475
- const orderEntry = config.enableOrderEntry === false ? false : caps.orderEntry;
11476
- const draggableOrders = config.enableDraggableOrders === false ? false : caps.draggableOrders;
11477
- const bracketOrders = config.enableBracketOrders === false ? false : caps.bracketOrders;
11478
- const managedTrading = config.enableManagedTrading === false ? false : caps.managedTrading;
11479
- const indicators = config.enableIndicators === false ? false : caps.indicators;
11480
- return {
11481
- ...caps,
11482
- orderEntry,
11483
- draggableOrders,
11484
- bracketOrders,
11485
- managedTrading,
11486
- indicators,
11487
- // Recalculate derived render flags from the updated feature flags.
11488
- renderOrderEntry: orderEntry,
11489
- renderBuySellButtons: orderEntry,
11490
- renderOrderTicket: orderEntry,
11491
- renderBracketControls: orderEntry && bracketOrders,
11492
- renderOrderModificationControls: orderEntry && draggableOrders,
11493
- renderManagedTradingControls: managedTrading,
11494
- renderIndicators: indicators
13023
+ function WatchlistDrawer({ onClose, onSelectSymbol, symbolResolver }) {
13024
+ const [groups, setGroups] = useState([]);
13025
+ const [items, setItems] = useState([]);
13026
+ const [loading, setLoading] = useState(true);
13027
+ const [searchOpen, setSearchOpen] = useState(false);
13028
+ const [addError, setAddError] = useState("");
13029
+ const [ctxMenu, setCtxMenu] = useState(null);
13030
+ const [groupDialog, setGroupDialog] = useState(null);
13031
+ const [groupName, setGroupName] = useState("");
13032
+ const [collapsed, setCollapsed] = useState(/* @__PURE__ */ new Set());
13033
+ const [logoMap, setLogoMap] = useState(/* @__PURE__ */ new Map());
13034
+ const [drawerWidth, setDrawerWidth] = useState(360);
13035
+ const [colWidths, setColWidths] = useState({ last: 70, chg: 70, pct: 60, vol: 52 });
13036
+ const [dragItemId, setDragItemId] = useState(null);
13037
+ const [dropTargetId, setDropTargetId] = useState(null);
13038
+ const ctxRef = useRef(null);
13039
+ const drawerRef = useRef(null);
13040
+ const dragState = useRef(null);
13041
+ const colDragRef = useRef(null);
13042
+ const onColResizeMouseDown = (e, col) => {
13043
+ e.preventDefault();
13044
+ e.stopPropagation();
13045
+ colDragRef.current = { col, startX: e.clientX, startW: colWidths[col] };
13046
+ const onMove = (ev) => {
13047
+ if (!colDragRef.current) return;
13048
+ const { col: c, startX, startW } = colDragRef.current;
13049
+ const next = Math.max(36, startW + (ev.clientX - startX));
13050
+ setColWidths((prev) => ({ ...prev, [c]: next }));
13051
+ };
13052
+ const onUp = () => {
13053
+ colDragRef.current = null;
13054
+ window.removeEventListener("mousemove", onMove);
13055
+ window.removeEventListener("mouseup", onUp);
13056
+ };
13057
+ window.addEventListener("mousemove", onMove);
13058
+ window.addEventListener("mouseup", onUp);
11495
13059
  };
11496
- }
11497
- function useChartCapabilities(config) {
11498
- const [licCaps, setLicCaps] = useState(() => getCapabilities());
13060
+ const onResizeMouseDown = (e) => {
13061
+ e.preventDefault();
13062
+ dragState.current = { startX: e.clientX, startW: drawerWidth };
13063
+ const onMove = (ev) => {
13064
+ if (!dragState.current) return;
13065
+ const delta = dragState.current.startX - ev.clientX;
13066
+ const next = Math.min(600, Math.max(240, dragState.current.startW + delta));
13067
+ setDrawerWidth(next);
13068
+ };
13069
+ const onUp = () => {
13070
+ dragState.current = null;
13071
+ window.removeEventListener("mousemove", onMove);
13072
+ window.removeEventListener("mouseup", onUp);
13073
+ };
13074
+ window.addEventListener("mousemove", onMove);
13075
+ window.addEventListener("mouseup", onUp);
13076
+ };
13077
+ const reload = useCallback(async () => {
13078
+ try {
13079
+ const data = await watchlistApi.getAll();
13080
+ setGroups(data.groups);
13081
+ setItems(data.items);
13082
+ } catch {
13083
+ } finally {
13084
+ setLoading(false);
13085
+ }
13086
+ }, []);
11499
13087
  useEffect(() => {
11500
- const unsub = LicenseManager.getInstance().subscribe(() => {
11501
- setLicCaps(getCapabilities());
13088
+ reload();
13089
+ }, [reload]);
13090
+ useEffect(() => {
13091
+ if (items.length === 0) return;
13092
+ const syms = [...new Set(items.map((i) => i.symbol))].join(",");
13093
+ fetch(`/api/reference/symbols/logos?symbols=${encodeURIComponent(syms)}`).then((r) => r.ok ? r.json() : {}).then((map) => setLogoMap(new Map(Object.entries(map)))).catch(() => {
11502
13094
  });
11503
- return unsub;
11504
- }, []);
11505
- return useMemo(() => applyRuntimeConfig(licCaps, config), [licCaps, config]);
11506
- }
11507
-
11508
- // src/react/trading/TradingBridge.ts
11509
- function createTradingBridgeLogger(callbacks, label) {
11510
- if (process.env.NODE_ENV === "production") return callbacks;
11511
- const tag = label ? `[TradingBridge:${label}]` : "[TradingBridge]";
11512
- return {
11513
- onPlaceOrderIntent: (intent) => {
11514
- console.debug(
11515
- `${tag} PLACE side=%s type=%s qty=%s price=%s stopPrice=%s correlationId=%s`,
11516
- intent.side,
11517
- intent.orderType,
11518
- intent.qty,
11519
- intent.limitPrice ?? "\u2014",
11520
- intent.stopPrice ?? "\u2014",
11521
- intent.correlationId,
11522
- intent
11523
- );
11524
- return callbacks.onPlaceOrderIntent?.(intent);
11525
- },
11526
- onModifyOrderIntent: (intent) => {
11527
- console.debug(
11528
- `${tag} MODIFY orderId=%s limitPrice=%s stopPrice=%s qty=%s`,
11529
- intent.orderId,
11530
- intent.limitPrice ?? "\u2014",
11531
- intent.stopPrice ?? "\u2014",
11532
- intent.qty ?? "\u2014",
11533
- intent
11534
- );
11535
- return callbacks.onModifyOrderIntent?.(intent);
11536
- },
11537
- onCancelOrderIntent: (intent) => {
11538
- console.debug(
11539
- `${tag} CANCEL orderId=%s symbol=%s`,
11540
- intent.orderId,
11541
- intent.symbol,
11542
- intent
11543
- );
11544
- return callbacks.onCancelOrderIntent?.(intent);
11545
- },
11546
- onBracketAdjustIntent: (intent) => {
11547
- console.debug(
11548
- `${tag} BRACKET_ADJUST entry=%s sl=%s tp=%s`,
11549
- intent.entryOrderId,
11550
- intent.stopLossPrice ?? "\u2014",
11551
- intent.takeProfitPrice ?? "\u2014",
11552
- intent
11553
- );
11554
- return callbacks.onBracketAdjustIntent?.(intent);
13095
+ }, [items]);
13096
+ const symbols = items.map((i) => i.symbol);
13097
+ const { prices: livePrices, dirs: liveDirs } = useWatchlistQuotes(symbols);
13098
+ const sessionOpenRef = useRef(/* @__PURE__ */ new Map());
13099
+ const quotes = useRef(/* @__PURE__ */ new Map()).current;
13100
+ quotes.clear();
13101
+ for (const [sym, last] of livePrices) {
13102
+ if (!sessionOpenRef.current.has(sym)) {
13103
+ sessionOpenRef.current.set(sym, last);
13104
+ }
13105
+ const open = sessionOpenRef.current.get(sym);
13106
+ const change = last - open;
13107
+ quotes.set(sym, {
13108
+ last,
13109
+ change,
13110
+ changePct: open !== 0 ? change / open * 100 : 0
13111
+ });
13112
+ }
13113
+ const prevItemsRef = useRef([]);
13114
+ if (prevItemsRef.current !== items) {
13115
+ const prevSyms = new Set(prevItemsRef.current.map((i) => i.symbol));
13116
+ const nextSyms = new Set(items.map((i) => i.symbol));
13117
+ for (const s of prevSyms) {
13118
+ if (!nextSyms.has(s)) sessionOpenRef.current.delete(s);
13119
+ }
13120
+ prevItemsRef.current = items;
13121
+ }
13122
+ useEffect(() => {
13123
+ if (!ctxMenu) return;
13124
+ const handler = (e) => {
13125
+ if (ctxRef.current && !ctxRef.current.contains(e.target)) setCtxMenu(null);
13126
+ };
13127
+ document.addEventListener("mousedown", handler);
13128
+ return () => document.removeEventListener("mousedown", handler);
13129
+ }, [ctxMenu]);
13130
+ const handleAddFromSearch = useCallback(async (symbol) => {
13131
+ setSearchOpen(false);
13132
+ setAddError("");
13133
+ try {
13134
+ await watchlistApi.addItem(symbol);
13135
+ await reload();
13136
+ } catch (err) {
13137
+ setAddError(err instanceof Error ? err.message : "Failed to add");
13138
+ }
13139
+ }, [reload]);
13140
+ const handleRemove = async (id) => {
13141
+ await watchlistApi.removeItem(id);
13142
+ await reload();
13143
+ };
13144
+ const openCtxMenu = (e, item) => {
13145
+ e.preventDefault();
13146
+ setCtxMenu({ x: e.clientX, y: e.clientY, itemId: item.id, symbol: item.symbol });
13147
+ };
13148
+ const openGroupDialog = (itemId) => {
13149
+ setCtxMenu(null);
13150
+ setGroupName("");
13151
+ setGroupDialog({ itemId });
13152
+ };
13153
+ const handleCreateGroup = async () => {
13154
+ const name = groupName.trim();
13155
+ if (!name || !groupDialog) return;
13156
+ const group = await watchlistApi.addGroup(name);
13157
+ await watchlistApi.moveItemToGroup(groupDialog.itemId, group.id);
13158
+ setGroupDialog(null);
13159
+ await reload();
13160
+ };
13161
+ const handleMoveToExisting = async (itemId, groupId) => {
13162
+ setCtxMenu(null);
13163
+ await watchlistApi.moveItemToGroup(itemId, groupId);
13164
+ await reload();
13165
+ };
13166
+ const handleUngroup = async (itemId) => {
13167
+ setCtxMenu(null);
13168
+ await watchlistApi.moveItemToGroup(itemId, null);
13169
+ await reload();
13170
+ };
13171
+ const handleDragStart = (e, itemId) => {
13172
+ setDragItemId(itemId);
13173
+ e.dataTransfer.effectAllowed = "move";
13174
+ };
13175
+ const handleDragOver = (e, targetId) => {
13176
+ e.preventDefault();
13177
+ e.dataTransfer.dropEffect = "move";
13178
+ if (targetId !== dragItemId) setDropTargetId(targetId);
13179
+ };
13180
+ const handleDrop = async (e, targetId) => {
13181
+ e.preventDefault();
13182
+ if (!dragItemId || dragItemId === targetId) return;
13183
+ setDragItemId(null);
13184
+ setDropTargetId(null);
13185
+ const reorder = (list) => {
13186
+ const from = list.findIndex((i) => i.id === dragItemId);
13187
+ const to = list.findIndex((i) => i.id === targetId);
13188
+ if (from === -1 || to === -1) return list;
13189
+ const next = [...list];
13190
+ const [moved] = next.splice(from, 1);
13191
+ next.splice(to, 0, moved);
13192
+ return next;
13193
+ };
13194
+ const newItems = (() => {
13195
+ const ungr = items.filter((i) => i.group_id === null);
13196
+ const newUngr = reorder(ungr);
13197
+ if (newUngr !== ungr) {
13198
+ return [...newUngr, ...items.filter((i) => i.group_id !== null)];
13199
+ }
13200
+ const result = [...items];
13201
+ for (const group of groups) {
13202
+ const grp = items.filter((i) => i.group_id === group.id);
13203
+ const newGrp = reorder(grp);
13204
+ if (newGrp !== grp) {
13205
+ const base = result.filter((i) => i.group_id !== group.id);
13206
+ return [...base, ...newGrp];
13207
+ }
13208
+ }
13209
+ return result;
13210
+ })();
13211
+ setItems(newItems);
13212
+ try {
13213
+ await watchlistApi.reorderItems(newItems.map((i) => i.id));
13214
+ } catch {
13215
+ await reload();
11555
13216
  }
11556
13217
  };
13218
+ const handleDragEnd = () => {
13219
+ setDragItemId(null);
13220
+ setDropTargetId(null);
13221
+ };
13222
+ const handleRemoveGroup = async (groupId) => {
13223
+ await watchlistApi.removeGroup(groupId);
13224
+ await reload();
13225
+ };
13226
+ const toggleCollapse = (groupId) => {
13227
+ setCollapsed((prev) => {
13228
+ const next = new Set(prev);
13229
+ next.has(groupId) ? next.delete(groupId) : next.add(groupId);
13230
+ return next;
13231
+ });
13232
+ };
13233
+ const ungrouped = items.filter((i) => i.group_id === null);
13234
+ return /* @__PURE__ */ jsxs(
13235
+ "div",
13236
+ {
13237
+ ref: drawerRef,
13238
+ className: "wl-drawer",
13239
+ style: {
13240
+ width: drawerWidth,
13241
+ "--wl-last": `${colWidths.last}px`,
13242
+ "--wl-chg": `${colWidths.chg}px`,
13243
+ "--wl-pct": `${colWidths.pct}px`,
13244
+ "--wl-vol": `${colWidths.vol}px`
13245
+ },
13246
+ children: [
13247
+ /* @__PURE__ */ jsx("div", { className: "wl-resize-handle", onMouseDown: onResizeMouseDown }),
13248
+ /* @__PURE__ */ jsxs("div", { className: "wl-header", children: [
13249
+ /* @__PURE__ */ jsxs("span", { className: "wl-header-title", children: [
13250
+ /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 16 16", width: "14", height: "14", stroke: "currentColor", fill: "none", strokeWidth: "1.5", children: [
13251
+ /* @__PURE__ */ jsx("rect", { x: "2", y: "3", width: "12", height: "1.5", rx: "0.5", fill: "currentColor", stroke: "none" }),
13252
+ /* @__PURE__ */ jsx("rect", { x: "2", y: "7", width: "9", height: "1.5", rx: "0.5", fill: "currentColor", stroke: "none" }),
13253
+ /* @__PURE__ */ jsx("rect", { x: "2", y: "11", width: "10", height: "1.5", rx: "0.5", fill: "currentColor", stroke: "none" })
13254
+ ] }),
13255
+ "Watchlist"
13256
+ ] }),
13257
+ /* @__PURE__ */ jsx("button", { className: "wl-close-btn", onClick: onClose, title: "Close", children: /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 14 14", width: "12", height: "12", stroke: "currentColor", strokeWidth: "1.8", children: [
13258
+ /* @__PURE__ */ jsx("line", { x1: "2", y1: "2", x2: "12", y2: "12" }),
13259
+ /* @__PURE__ */ jsx("line", { x1: "12", y1: "2", x2: "2", y2: "12" })
13260
+ ] }) })
13261
+ ] }),
13262
+ /* @__PURE__ */ jsxs("div", { className: "wl-add-row", children: [
13263
+ /* @__PURE__ */ jsx(
13264
+ "input",
13265
+ {
13266
+ readOnly: true,
13267
+ className: "wl-add-input",
13268
+ placeholder: "Add symbol e.g. BINANCE:BTCUSDT",
13269
+ onClick: () => setSearchOpen(true),
13270
+ onFocus: () => setSearchOpen(true),
13271
+ style: { cursor: "pointer" }
13272
+ }
13273
+ ),
13274
+ /* @__PURE__ */ jsx("button", { className: "wl-add-btn", onClick: () => setSearchOpen(true), title: "Add", children: /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 14 14", width: "12", height: "12", stroke: "currentColor", strokeWidth: "2", children: [
13275
+ /* @__PURE__ */ jsx("line", { x1: "7", y1: "1", x2: "7", y2: "13" }),
13276
+ /* @__PURE__ */ jsx("line", { x1: "1", y1: "7", x2: "13", y2: "7" })
13277
+ ] }) })
13278
+ ] }),
13279
+ addError && /* @__PURE__ */ jsx("div", { className: "wl-add-error", children: addError }),
13280
+ /* @__PURE__ */ jsxs("div", { className: "wl-col-headers", children: [
13281
+ /* @__PURE__ */ jsx("span", {}),
13282
+ /* @__PURE__ */ jsx("span", { className: "wl-col-symbol", children: "Symbol" }),
13283
+ /* @__PURE__ */ jsxs("span", { className: "wl-col-last", children: [
13284
+ "Last",
13285
+ /* @__PURE__ */ jsx("div", { className: "wl-col-resize", onMouseDown: (e) => onColResizeMouseDown(e, "last") })
13286
+ ] }),
13287
+ /* @__PURE__ */ jsxs("span", { className: "wl-col-chg", children: [
13288
+ "Chg",
13289
+ /* @__PURE__ */ jsx("div", { className: "wl-col-resize", onMouseDown: (e) => onColResizeMouseDown(e, "chg") })
13290
+ ] }),
13291
+ /* @__PURE__ */ jsxs("span", { className: "wl-col-pct", children: [
13292
+ "Chg%",
13293
+ /* @__PURE__ */ jsx("div", { className: "wl-col-resize", onMouseDown: (e) => onColResizeMouseDown(e, "pct") })
13294
+ ] }),
13295
+ /* @__PURE__ */ jsxs("span", { className: "wl-col-vol", children: [
13296
+ "Vol",
13297
+ /* @__PURE__ */ jsx("div", { className: "wl-col-resize", onMouseDown: (e) => onColResizeMouseDown(e, "vol") })
13298
+ ] })
13299
+ ] }),
13300
+ /* @__PURE__ */ jsxs("div", { className: "wl-list", children: [
13301
+ loading && /* @__PURE__ */ jsx("div", { className: "wl-empty", children: "Loading\u2026" }),
13302
+ groups.map((group) => {
13303
+ const groupItems = items.filter((i) => i.group_id === group.id);
13304
+ const isCollapsed = collapsed.has(group.id);
13305
+ return /* @__PURE__ */ jsxs("div", { className: "wl-group", children: [
13306
+ /* @__PURE__ */ jsxs("div", { className: "wl-group-header", children: [
13307
+ /* @__PURE__ */ jsx(
13308
+ "button",
13309
+ {
13310
+ className: "wl-group-toggle",
13311
+ onClick: () => toggleCollapse(group.id),
13312
+ title: isCollapsed ? "Expand" : "Collapse",
13313
+ children: /* @__PURE__ */ jsx(
13314
+ "svg",
13315
+ {
13316
+ viewBox: "0 0 10 10",
13317
+ width: "8",
13318
+ height: "8",
13319
+ stroke: "currentColor",
13320
+ fill: "none",
13321
+ strokeWidth: "1.5",
13322
+ style: { transform: isCollapsed ? "rotate(-90deg)" : "none", transition: "transform 0.15s" },
13323
+ children: /* @__PURE__ */ jsx("polyline", { points: "2,3 5,7 8,3" })
13324
+ }
13325
+ )
13326
+ }
13327
+ ),
13328
+ /* @__PURE__ */ jsx("span", { className: "wl-group-name", children: group.name }),
13329
+ /* @__PURE__ */ jsx("span", { className: "wl-group-count", children: groupItems.length }),
13330
+ /* @__PURE__ */ jsx(
13331
+ "button",
13332
+ {
13333
+ className: "wl-remove-btn wl-group-del",
13334
+ title: "Remove group (items become ungrouped)",
13335
+ onClick: () => handleRemoveGroup(group.id),
13336
+ children: "\u2715"
13337
+ }
13338
+ )
13339
+ ] }),
13340
+ !isCollapsed && groupItems.map((item) => /* @__PURE__ */ jsx(
13341
+ SymbolRow,
13342
+ {
13343
+ item,
13344
+ quote: quotes.get(item.symbol),
13345
+ flash: liveDirs.get(item.symbol),
13346
+ dragOver: dropTargetId === item.id,
13347
+ ...logoMap.get(item.symbol) !== void 0 && { logoUrl: logoMap.get(item.symbol) },
13348
+ onSelect: () => onSelectSymbol(item.symbol),
13349
+ onRemove: () => handleRemove(item.id),
13350
+ onContextMenu: (e) => openCtxMenu(e, item),
13351
+ onDragStart: (e) => handleDragStart(e, item.id),
13352
+ onDragOver: (e) => handleDragOver(e, item.id),
13353
+ onDrop: (e) => handleDrop(e, item.id),
13354
+ onDragEnd: handleDragEnd
13355
+ },
13356
+ item.id
13357
+ ))
13358
+ ] }, group.id);
13359
+ }),
13360
+ ungrouped.length > 0 && groups.length > 0 && /* @__PURE__ */ jsx("div", { className: "wl-group-label-ungrouped", children: "Other" }),
13361
+ ungrouped.map((item) => /* @__PURE__ */ jsx(
13362
+ SymbolRow,
13363
+ {
13364
+ item,
13365
+ quote: quotes.get(item.symbol),
13366
+ flash: liveDirs.get(item.symbol),
13367
+ dragOver: dropTargetId === item.id,
13368
+ ...logoMap.get(item.symbol) !== void 0 && { logoUrl: logoMap.get(item.symbol) },
13369
+ onSelect: () => onSelectSymbol(item.symbol),
13370
+ onRemove: () => handleRemove(item.id),
13371
+ onContextMenu: (e) => openCtxMenu(e, item),
13372
+ onDragStart: (e) => handleDragStart(e, item.id),
13373
+ onDragOver: (e) => handleDragOver(e, item.id),
13374
+ onDrop: (e) => handleDrop(e, item.id),
13375
+ onDragEnd: handleDragEnd
13376
+ },
13377
+ item.id
13378
+ )),
13379
+ !loading && items.length === 0 && /* @__PURE__ */ jsxs("div", { className: "wl-empty", children: [
13380
+ "No symbols yet.",
13381
+ /* @__PURE__ */ jsx("br", {}),
13382
+ "Type a symbol above and press Enter."
13383
+ ] })
13384
+ ] }),
13385
+ ctxMenu && /* @__PURE__ */ jsxs(
13386
+ "div",
13387
+ {
13388
+ ref: ctxRef,
13389
+ className: "wl-ctx-menu",
13390
+ style: { top: ctxMenu.y, left: ctxMenu.x },
13391
+ children: [
13392
+ groups.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
13393
+ /* @__PURE__ */ jsx("div", { className: "wl-ctx-section", children: "Move to group" }),
13394
+ groups.map((g) => /* @__PURE__ */ jsx(
13395
+ "button",
13396
+ {
13397
+ className: "wl-ctx-item",
13398
+ onClick: () => handleMoveToExisting(ctxMenu.itemId, g.id),
13399
+ children: g.name
13400
+ },
13401
+ g.id
13402
+ )),
13403
+ /* @__PURE__ */ jsx("div", { className: "wl-ctx-divider" })
13404
+ ] }),
13405
+ /* @__PURE__ */ jsx(
13406
+ "button",
13407
+ {
13408
+ className: "wl-ctx-item",
13409
+ onClick: () => openGroupDialog(ctxMenu.itemId),
13410
+ children: "New group\u2026"
13411
+ }
13412
+ ),
13413
+ items.find((i) => i.id === ctxMenu.itemId)?.group_id && /* @__PURE__ */ jsx(
13414
+ "button",
13415
+ {
13416
+ className: "wl-ctx-item",
13417
+ onClick: () => handleUngroup(ctxMenu.itemId),
13418
+ children: "Remove from group"
13419
+ }
13420
+ ),
13421
+ /* @__PURE__ */ jsx("div", { className: "wl-ctx-divider" }),
13422
+ /* @__PURE__ */ jsx(
13423
+ "button",
13424
+ {
13425
+ className: "wl-ctx-item wl-ctx-item--danger",
13426
+ onClick: () => {
13427
+ setCtxMenu(null);
13428
+ handleRemove(ctxMenu.itemId);
13429
+ },
13430
+ children: "Remove symbol"
13431
+ }
13432
+ )
13433
+ ]
13434
+ }
13435
+ ),
13436
+ groupDialog && /* @__PURE__ */ jsx("div", { className: "wl-dialog-backdrop", children: /* @__PURE__ */ jsxs("div", { className: "wl-dialog", children: [
13437
+ /* @__PURE__ */ jsx("div", { className: "wl-dialog-title", children: "Add to group" }),
13438
+ /* @__PURE__ */ jsx(
13439
+ "input",
13440
+ {
13441
+ className: "wl-dialog-input",
13442
+ placeholder: "Group name",
13443
+ value: groupName,
13444
+ onChange: (e) => setGroupName(e.target.value),
13445
+ onKeyDown: (e) => {
13446
+ if (e.key === "Enter") handleCreateGroup();
13447
+ },
13448
+ autoFocus: true
13449
+ }
13450
+ ),
13451
+ /* @__PURE__ */ jsxs("div", { className: "wl-dialog-actions", children: [
13452
+ /* @__PURE__ */ jsx("button", { className: "wl-dialog-cancel", onClick: () => setGroupDialog(null), children: "Cancel" }),
13453
+ /* @__PURE__ */ jsx("button", { className: "wl-dialog-confirm", onClick: handleCreateGroup, disabled: !groupName.trim(), children: "Save" })
13454
+ ] })
13455
+ ] }) }),
13456
+ searchOpen && /* @__PURE__ */ jsx(
13457
+ SymbolSearchDialog,
13458
+ {
13459
+ current: "",
13460
+ onSelect: handleAddFromSearch,
13461
+ onClose: () => setSearchOpen(false),
13462
+ symbolResolver
13463
+ }
13464
+ )
13465
+ ]
13466
+ }
13467
+ );
13468
+ }
13469
+ var BROKERS = [
13470
+ { id: "rithmic", name: "Rithmic", description: "Futures & Forex", status: "available" },
13471
+ { id: "ibkr", name: "Interactive Brokers", description: "Stocks, Options & More", status: "coming-soon" },
13472
+ { id: "tradovate", name: "Tradovate", description: "Futures & Options", status: "coming-soon" },
13473
+ { id: "alpaca", name: "Alpaca", description: "Stocks & Crypto", status: "coming-soon" },
13474
+ { id: "tradestation", name: "TradeStation", description: "Stocks, Futures & Options", status: "coming-soon" },
13475
+ { id: "tradier", name: "Tradier", description: "Stocks & Options", status: "coming-soon" },
13476
+ { id: "ninjabrokerage", name: "NinjaTrader Brokerage", description: "Futures & Forex", status: "coming-soon" },
13477
+ { id: "tastytrade", name: "tastytrade", description: "Options & Futures", status: "coming-soon" }
13478
+ ];
13479
+ function TradeDrawer({ onClose }) {
13480
+ return /* @__PURE__ */ jsxs("div", { className: "trade-drawer", children: [
13481
+ /* @__PURE__ */ jsxs("div", { className: "trade-drawer-header", children: [
13482
+ /* @__PURE__ */ jsxs("span", { className: "trade-drawer-title", children: [
13483
+ /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 16 16", width: "14", height: "14", fill: "none", stroke: "currentColor", strokeWidth: "1.5", children: [
13484
+ /* @__PURE__ */ jsx("rect", { x: "1.5", y: "3", width: "13", height: "10", rx: "1.5" }),
13485
+ /* @__PURE__ */ jsx("line", { x1: "5", y1: "8", x2: "11", y2: "8" }),
13486
+ /* @__PURE__ */ jsx("line", { x1: "8", y1: "5", x2: "8", y2: "11" })
13487
+ ] }),
13488
+ "Trade \u2014 Connect a broker"
13489
+ ] }),
13490
+ /* @__PURE__ */ jsx("button", { className: "trade-drawer-close", onClick: onClose, title: "Close", children: /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 14 14", width: "12", height: "12", stroke: "currentColor", strokeWidth: "1.8", children: [
13491
+ /* @__PURE__ */ jsx("line", { x1: "2", y1: "2", x2: "12", y2: "12" }),
13492
+ /* @__PURE__ */ jsx("line", { x1: "12", y1: "2", x2: "2", y2: "12" })
13493
+ ] }) })
13494
+ ] }),
13495
+ /* @__PURE__ */ jsx("div", { className: "trade-drawer-body", children: BROKERS.map((broker) => /* @__PURE__ */ jsxs("div", { className: "broker-card", children: [
13496
+ /* @__PURE__ */ jsx("div", { className: "broker-card-name", children: broker.name }),
13497
+ /* @__PURE__ */ jsx("div", { className: "broker-card-desc", children: broker.description }),
13498
+ /* @__PURE__ */ jsx("span", { className: `broker-card-status broker-card-status--${broker.status}`, children: broker.status === "available" ? "Available" : "Coming Soon" }),
13499
+ /* @__PURE__ */ jsx(
13500
+ "button",
13501
+ {
13502
+ className: "broker-card-connect",
13503
+ disabled: broker.status !== "available",
13504
+ title: broker.status === "available" ? `Connect ${broker.name}` : "Coming soon",
13505
+ children: broker.status === "available" ? "Connect" : "Coming Soon"
13506
+ }
13507
+ )
13508
+ ] }, broker.id)) })
13509
+ ] });
11557
13510
  }
11558
13511
  function uid() {
11559
13512
  return Math.random().toString(36).slice(2, 10);
@@ -11642,16 +13595,11 @@ var ManagedAppShell = forwardRef(
11642
13595
  const [isFullscreen, setIsFullscreen] = useState(false);
11643
13596
  const capabilities = useChartCapabilities(config);
11644
13597
  useEffect(() => {
11645
- if (process.env.NODE_ENV === "production") return;
11646
- if ((capabilities.orderEntry || capabilities.draggableOrders || capabilities.renderManagedTradingControls) && !renderTradeDrawer) {
11647
- console.warn(
11648
- "[ManagedAppShell] Trading capabilities are active (orderEntry=%s, draggableOrders=%s, managedTrading=%s) but renderTradeDrawer was not provided. Trading UI will be suppressed.",
11649
- capabilities.orderEntry,
11650
- capabilities.draggableOrders,
11651
- capabilities.managedTrading
11652
- );
11653
- }
11654
- }, [capabilities.orderEntry, capabilities.draggableOrders, capabilities.managedTrading]);
13598
+ watchlistApi.configure({
13599
+ apiBase: `${apiUrl ?? ""}/api`,
13600
+ ...getAuthToken ? { getAuthToken } : {}
13601
+ });
13602
+ }, [apiUrl, getAuthToken]);
11655
13603
  const [isLicensed, setIsLicensed] = useState(() => LicenseManager.getInstance().getLicense() !== null);
11656
13604
  useEffect(() => LicenseManager.getInstance().subscribe(() => {
11657
13605
  setIsLicensed(LicenseManager.getInstance().getLicense() !== null);
@@ -11971,21 +13919,11 @@ var ManagedAppShell = forwardRef(
11971
13919
  tab.id
11972
13920
  )) });
11973
13921
  const drawers = /* @__PURE__ */ jsxs(Fragment, { children: [
11974
- scriptDrawerOpen && renderScriptDrawer?.({
11975
- onClose: () => setScriptDrawerOpen(false),
11976
- onAddIndicator: handleAddIndicator
11977
- }),
11978
- watchlistOpen && renderWatchlistDrawer?.({
11979
- onClose: () => setWatchlistOpen(false),
11980
- onSelectSymbol: (sym) => {
11981
- handleSymbolChange(sym);
11982
- }
11983
- })
13922
+ scriptDrawerOpen && (renderScriptDrawer ? renderScriptDrawer({ onClose: () => setScriptDrawerOpen(false), onAddIndicator: handleAddIndicator }) : /* @__PURE__ */ jsx(ScriptDrawer, { onClose: () => setScriptDrawerOpen(false), onAddIndicator: handleAddIndicator })),
13923
+ watchlistOpen && (renderWatchlistDrawer ? renderWatchlistDrawer({ onClose: () => setWatchlistOpen(false), onSelectSymbol: handleSymbolChange }) : /* @__PURE__ */ jsx(WatchlistDrawer, { onClose: () => setWatchlistOpen(false), onSelectSymbol: handleSymbolChange, symbolResolver }))
11984
13924
  ] });
11985
13925
  return /* @__PURE__ */ jsxs("div", { ref: containerRef, style: { height: "100%" }, children: [
11986
- tradeDrawerOpen && capabilities.renderManagedTradingControls && renderTradeDrawer?.({
11987
- onClose: () => setTradeDrawerOpen(false)
11988
- }),
13926
+ tradeDrawerOpen && capabilities.renderManagedTradingControls && (renderTradeDrawer ? renderTradeDrawer({ onClose: () => setTradeDrawerOpen(false) }) : /* @__PURE__ */ jsx(TradeDrawer, { onClose: () => setTradeDrawerOpen(false) })),
11989
13927
  /* @__PURE__ */ jsx(
11990
13928
  ChartWorkspace,
11991
13929
  {